[REF] sale,*: remove locked (done) SO state
It's not a specific state, it should be considered separately, as a boolean. This allows to reduce code complexity (flows should not rely (much) on the locked logic) and to have a clearer flow. Task-3163931 Part-of: odoo/odoo#115871
This commit is contained in:
@@ -11,31 +11,32 @@
|
||||
<field name="is_all_service" invisible="1"/>
|
||||
<field name="recompute_delivery_price" invisible="1"/>
|
||||
</field>
|
||||
<group name="note_group" position="before">
|
||||
<div class="oe_right mb-2 ms-1">
|
||||
<button
|
||||
string="Add shipping"
|
||||
name="action_open_delivery_wizard"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', '|',('is_all_service', '=', True), ('order_line', '=', []), ('delivery_set', '=', True)]}"
|
||||
/>
|
||||
<button
|
||||
string="Update shipping cost"
|
||||
name="action_open_delivery_wizard"
|
||||
context="{'carrier_recompute':True}"
|
||||
type="object"
|
||||
class="text-warning btn-secondary"
|
||||
attrs="{'invisible': ['|', '|',('is_all_service', '=', True), ('recompute_delivery_price', '=', False), ('delivery_set', '=', False)]}"
|
||||
/>
|
||||
<button
|
||||
string="Update shipping cost"
|
||||
name="action_open_delivery_wizard"
|
||||
context="{'carrier_recompute':True}"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', '|',('is_all_service', '=', True), ('recompute_delivery_price', '=', True), ('delivery_set', '=', False)]}"
|
||||
/>
|
||||
</div>
|
||||
</group>
|
||||
<div name="so_button_below_order_lines" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</div>
|
||||
<div name="so_button_below_order_lines" position="inside">
|
||||
<button
|
||||
string="Add shipping"
|
||||
name="action_open_delivery_wizard"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', '|',('is_all_service', '=', True), ('order_line', '=', []), ('delivery_set', '=', True)]}"
|
||||
/>
|
||||
<button
|
||||
string="Update shipping cost"
|
||||
name="action_open_delivery_wizard"
|
||||
context="{'carrier_recompute':True}"
|
||||
type="object"
|
||||
class="text-warning btn-secondary"
|
||||
attrs="{'invisible': ['|', '|',('is_all_service', '=', True), ('recompute_delivery_price', '=', False), ('delivery_set', '=', False)]}"
|
||||
/>
|
||||
<button
|
||||
string="Update shipping cost"
|
||||
name="action_open_delivery_wizard"
|
||||
context="{'carrier_recompute':True}"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', '|',('is_all_service', '=', True), ('recompute_delivery_price', '=', True), ('delivery_set', '=', False)]}"
|
||||
/>
|
||||
</div>
|
||||
<xpath expr="//field[@name='order_line']/form/group/group/field[@name='price_unit']" position="before">
|
||||
<field name="recompute_delivery_price" invisible="1"/>
|
||||
<field name="is_delivery" invisible="1"/>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models, tools
|
||||
from odoo.addons.sale.models.sale_order import SALE_ORDER_STATE
|
||||
|
||||
|
||||
class EventSaleReport(models.Model):
|
||||
@@ -31,13 +32,8 @@ class EventSaleReport(models.Model):
|
||||
sale_order_id = fields.Many2one('sale.order', readonly=True)
|
||||
sale_order_date = fields.Datetime('Order Date', readonly=True)
|
||||
sale_order_partner_id = fields.Many2one('res.partner', string='Customer', readonly=True)
|
||||
sale_order_state = fields.Selection([
|
||||
('draft', 'Quotation'),
|
||||
('sent', 'Quotation Sent'),
|
||||
('sale', 'Sales Order'),
|
||||
('done', 'Locked'),
|
||||
('cancel', 'Cancelled'),
|
||||
], string='Sale Order Status', readonly=True)
|
||||
sale_order_state = fields.Selection(
|
||||
selection=SALE_ORDER_STATE, string='Sale Order Status', readonly=True)
|
||||
sale_order_user_id = fields.Many2one('res.users', string='Salesperson', readonly=True)
|
||||
sale_order_line_id = fields.Many2one('sale.order.line', readonly=True)
|
||||
sale_price = fields.Float('Revenues', readonly=True)
|
||||
@@ -66,7 +62,7 @@ class EventSaleReport(models.Model):
|
||||
def _with_clause(self, *with_):
|
||||
# Extra clauses formatted as `cte1 AS (SELECT ...)`, `cte2 AS (SELECT ...)`...
|
||||
return """
|
||||
WITH
|
||||
WITH
|
||||
""" + ',\n '.join(with_) if with_ else ''
|
||||
|
||||
def _select_clause(self, *select):
|
||||
@@ -74,19 +70,19 @@ WITH
|
||||
return """
|
||||
SELECT
|
||||
ROW_NUMBER() OVER (ORDER BY event_registration.id) AS id,
|
||||
|
||||
|
||||
event_registration.id AS event_registration_id,
|
||||
event_registration.company_id AS company_id,
|
||||
event_registration.event_id AS event_id,
|
||||
event_registration.event_ticket_id AS event_ticket_id,
|
||||
event_registration.create_date AS event_registration_create_date,
|
||||
event_registration.name AS event_registration_name,
|
||||
event_registration.state AS event_registration_state,
|
||||
event_registration.state AS event_registration_state,
|
||||
event_registration.active AS active,
|
||||
event_registration.sale_order_id AS sale_order_id,
|
||||
event_registration.sale_order_line_id AS sale_order_line_id,
|
||||
event_registration.is_paid AS is_paid,
|
||||
|
||||
|
||||
event_event.event_type_id AS event_type_id,
|
||||
event_event.date_begin AS event_date_begin,
|
||||
event_event.date_end AS event_date_end,
|
||||
@@ -98,7 +94,7 @@ SELECT
|
||||
sale_order.partner_id AS sale_order_partner_id,
|
||||
sale_order.state AS sale_order_state,
|
||||
sale_order.user_id AS sale_order_user_id,
|
||||
|
||||
|
||||
sale_order_line.product_id AS product_id,
|
||||
CASE
|
||||
WHEN sale_order_line.product_uom_qty = 0 THEN 0
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
<field name="invoice_partner_id" optional="hide"/>
|
||||
<field name="event_registration_name" optional="hide"/>
|
||||
<field name="sale_order_state" widget="badge"
|
||||
decoration-success="sale_order_state == 'sale' or sale_order_state == 'done'"
|
||||
decoration-success="sale_order_state == 'sale'"
|
||||
decoration-info="sale_order_state == 'draft' or sale_order_state == 'sent'"/>
|
||||
</tree>
|
||||
</field>
|
||||
|
||||
@@ -35,7 +35,7 @@ class Project(models.Model):
|
||||
[
|
||||
('order_id', 'in', list(expenses_per_so_id.keys())),
|
||||
('is_expense', '=', True),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
],
|
||||
['order_id', 'product_id'],
|
||||
['untaxed_amount_to_invoice:sum', 'untaxed_amount_invoiced:sum'])
|
||||
|
||||
@@ -40,7 +40,7 @@ class CustomerPortal(portal.CustomerPortal):
|
||||
def _prepare_orders_domain(self, partner):
|
||||
return [
|
||||
('message_partner_ids', 'child_of', [partner.commercial_partner_id.id]),
|
||||
('state', 'in', ['sale', 'done'])
|
||||
('state', '=', 'sale'),
|
||||
]
|
||||
|
||||
def _get_sale_searchbar_sortings(self):
|
||||
|
||||
@@ -63,17 +63,16 @@ class AccountMove(models.Model):
|
||||
res = super(AccountMove, self).action_post()
|
||||
down_payment_lines = self.line_ids.filtered('is_downpayment')
|
||||
for line in down_payment_lines:
|
||||
if any(order.locked for order in line.sale_line_ids.order_id):
|
||||
# We cannot change lines content on locked SO, changes on invoices are not
|
||||
# forwarded to the SO if the SO is locked.
|
||||
continue
|
||||
|
||||
if not line.sale_line_ids.display_type:
|
||||
line.sale_line_ids._compute_name()
|
||||
|
||||
try:
|
||||
line.sale_line_ids.tax_id = line.tax_ids
|
||||
line.sale_line_ids.price_unit = line.price_unit
|
||||
except UserError:
|
||||
# a UserError here means the SO was locked, which prevents changing the taxes
|
||||
# just ignore the error - this is a nice to have feature and should not be blocking
|
||||
pass
|
||||
line.sale_line_ids.tax_id = line.tax_ids
|
||||
line.sale_line_ids.price_unit = line.price_unit
|
||||
return res
|
||||
|
||||
def button_draft(self):
|
||||
|
||||
@@ -76,15 +76,28 @@ class AccountMoveLine(models.Model):
|
||||
continue
|
||||
|
||||
# raise if the sale order is not currently open
|
||||
if sale_order.state != 'sale':
|
||||
message_unconfirmed = _('The Sales Order %s linked to the Analytic Account %s must be validated before registering expenses.')
|
||||
messages = {
|
||||
'draft': message_unconfirmed,
|
||||
'sent': message_unconfirmed,
|
||||
'done': _('The Sales Order %s linked to the Analytic Account %s is currently locked. You cannot register an expense on a locked Sales Order. Please create a new SO linked to this Analytic Account.'),
|
||||
'cancel': _('The Sales Order %s linked to the Analytic Account %s is cancelled. You cannot register an expense on a cancelled Sales Order.'),
|
||||
}
|
||||
raise UserError(messages[sale_order.state] % (sale_order.name, sale_order.analytic_account_id.name))
|
||||
if sale_order.state in ('draft', 'sent'):
|
||||
raise UserError(_(
|
||||
"The Sales Order %(order)s linked to the Analytic Account %(account)s must be"
|
||||
" validated before registering expenses.",
|
||||
order=sale_order.name,
|
||||
account=sale_order.analytic_account_id.name,
|
||||
))
|
||||
elif sale_order.state == 'cancel':
|
||||
raise UserError(_(
|
||||
"The Sales Order %(order)s linked to the Analytic Account %(account)s is cancelled."
|
||||
" You cannot register an expense on a cancelled Sales Order.",
|
||||
order=sale_order.name,
|
||||
account=sale_order.analytic_account_id.name,
|
||||
))
|
||||
elif sale_order.locked:
|
||||
raise UserError(_(
|
||||
"The Sales Order %(order)s linked to the Analytic Account %(account)s is currently locked."
|
||||
" You cannot register an expense on a locked Sales Order."
|
||||
" Please create a new SO linked to this Analytic Account.",
|
||||
order=sale_order.name,
|
||||
account=sale_order.analytic_account_id.name,
|
||||
))
|
||||
|
||||
price = move_line._sale_get_invoice_price(sale_order)
|
||||
|
||||
|
||||
@@ -130,7 +130,7 @@ class CrmTeam(models.Model):
|
||||
|
||||
def _extra_sql_conditions(self):
|
||||
if self._in_sale_scope():
|
||||
return "AND state in ('sale', 'done')"
|
||||
return "AND state = 'sale'"
|
||||
return super()._extra_sql_conditions()
|
||||
|
||||
def _graph_title_and_key(self):
|
||||
|
||||
@@ -151,7 +151,7 @@ class PaymentTransaction(models.Model):
|
||||
('is_move_sent', '=', False),
|
||||
('state', '=', 'posted'),
|
||||
])),
|
||||
('sale_order_ids.state', 'in', ('sale', 'done')),
|
||||
('sale_order_ids.state', '=', 'sale'),
|
||||
('last_state_change', '>=', retry_limit_date),
|
||||
])._send_invoice()
|
||||
|
||||
@@ -159,7 +159,7 @@ class PaymentTransaction(models.Model):
|
||||
for tx in self.filtered(lambda tx: tx.sale_order_ids):
|
||||
# Create invoices
|
||||
tx = tx.with_company(tx.company_id).with_context(company_id=tx.company_id.id)
|
||||
confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state in ('sale', 'done'))
|
||||
confirmed_orders = tx.sale_order_ids.filtered(lambda so: so.state == 'sale')
|
||||
if confirmed_orders:
|
||||
confirmed_orders._force_lines_to_invoice_policy_order()
|
||||
invoices = confirmed_orders.with_context(
|
||||
|
||||
@@ -44,7 +44,7 @@ class ResPartner(models.Model):
|
||||
SaleOrder = self.env['sale.order']
|
||||
has_so = SaleOrder.search([
|
||||
('partner_id', 'child_of', self.commercial_partner_id.id),
|
||||
('state', 'in', ['sent', 'sale', 'done'])
|
||||
('state', 'in', ['sent', 'sale'])
|
||||
], limit=1)
|
||||
return can_edit_vat and not bool(has_so)
|
||||
|
||||
@@ -56,7 +56,7 @@ class ResPartner(models.Model):
|
||||
|
||||
def _credit_debit_get(self):
|
||||
super()._credit_debit_get()
|
||||
domain = [('partner_id', 'in', self.ids), ('state', 'in', ['sale', 'done'])]
|
||||
domain = [('partner_id', 'in', self.ids), ('state', '=', 'sale')]
|
||||
group = self.env['sale.order']._read_group(domain, ['partner_id'], ['amount_to_invoice:sum'])
|
||||
for partner, amount_to_invoice_sum in group:
|
||||
partner.credit += amount_to_invoice_sum
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
@@ -8,7 +7,6 @@ from markupsafe import escape
|
||||
from odoo import api, fields, models, SUPERUSER_ID, _
|
||||
from odoo.exceptions import AccessError, UserError, ValidationError
|
||||
from odoo.fields import Command
|
||||
from odoo.osv import expression
|
||||
from odoo.tools import float_is_zero, format_amount, format_date, html_keep_url, is_html_empty
|
||||
from odoo.tools.sql import create_index
|
||||
|
||||
@@ -16,12 +14,7 @@ from odoo.addons.payment import utils as payment_utils
|
||||
|
||||
READONLY_FIELD_STATES = {
|
||||
state: [('readonly', True)]
|
||||
for state in {'sale', 'done', 'cancel'}
|
||||
}
|
||||
|
||||
LOCKED_FIELD_STATES = {
|
||||
state: [('readonly', True)]
|
||||
for state in {'done', 'cancel'}
|
||||
for state in {'sale', 'cancel'}
|
||||
}
|
||||
|
||||
INVOICE_STATUS = [
|
||||
@@ -31,6 +24,13 @@ INVOICE_STATUS = [
|
||||
('no', 'Nothing to Invoice')
|
||||
]
|
||||
|
||||
SALE_ORDER_STATE = [
|
||||
('draft', "Quotation"),
|
||||
('sent', "Quotation Sent"),
|
||||
('sale', "Sales Order"),
|
||||
('cancel', "Cancelled"),
|
||||
]
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_name = 'sale.order'
|
||||
@@ -41,7 +41,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
_sql_constraints = [
|
||||
('date_order_conditional_required',
|
||||
"CHECK((state IN ('sale', 'done') AND date_order IS NOT NULL) OR state NOT IN ('sale', 'done'))",
|
||||
"CHECK((state = 'sale' AND date_order IS NOT NULL) OR state != 'sale')",
|
||||
"A confirmed sales order requires a confirmation date."),
|
||||
]
|
||||
|
||||
@@ -72,24 +72,18 @@ class SaleOrder(models.Model):
|
||||
states=READONLY_FIELD_STATES,
|
||||
domain="[('type', '!=', 'private'), ('company_id', 'in', (False, company_id))]")
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('draft', "Quotation"),
|
||||
('sent', "Quotation Sent"),
|
||||
('sale', "Sales Order"),
|
||||
('done', "Locked"),
|
||||
('cancel', "Cancelled"),
|
||||
],
|
||||
selection=SALE_ORDER_STATE,
|
||||
string="Status",
|
||||
readonly=True, copy=False, index=True,
|
||||
tracking=3,
|
||||
default='draft')
|
||||
locked = fields.Boolean(default=False, copy=False, help="Locked orders cannot be modified.")
|
||||
|
||||
client_order_ref = fields.Char(string="Customer Reference", copy=False)
|
||||
create_date = fields.Datetime( # Override of default create_date field from ORM
|
||||
string="Creation Date", index=True, readonly=True)
|
||||
commitment_date = fields.Datetime(
|
||||
string="Delivery Date", copy=False,
|
||||
states=LOCKED_FIELD_STATES,
|
||||
help="This is the delivery date promised to the customer. "
|
||||
"If set, the delivery order will be scheduled based on "
|
||||
"this date rather than product lead times.")
|
||||
@@ -144,14 +138,12 @@ class SaleOrder(models.Model):
|
||||
string="Invoice Address",
|
||||
compute='_compute_partner_invoice_id',
|
||||
store=True, readonly=False, required=True, precompute=True,
|
||||
states=LOCKED_FIELD_STATES,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]")
|
||||
partner_shipping_id = fields.Many2one(
|
||||
comodel_name='res.partner',
|
||||
string="Delivery Address",
|
||||
compute='_compute_partner_shipping_id',
|
||||
store=True, readonly=False, required=True, precompute=True,
|
||||
states=LOCKED_FIELD_STATES,
|
||||
domain="['|', ('company_id', '=', False), ('company_id', '=', company_id)]",)
|
||||
|
||||
fiscal_position_id = fields.Many2one(
|
||||
@@ -212,7 +204,6 @@ class SaleOrder(models.Model):
|
||||
comodel_name='sale.order.line',
|
||||
inverse_name='order_id',
|
||||
string="Order Lines",
|
||||
states=LOCKED_FIELD_STATES,
|
||||
copy=True, auto_join=True)
|
||||
|
||||
amount_untaxed = fields.Monetary(string="Untaxed Amount", store=True, compute='_compute_amounts', tracking=5)
|
||||
@@ -525,9 +516,8 @@ class SaleOrder(models.Model):
|
||||
- invoiced: if all SO lines are invoiced, the SO is invoiced.
|
||||
- upselling: if all SO lines are invoiced or upselling, the status is upselling.
|
||||
"""
|
||||
unconfirmed_orders = self.filtered(lambda so: so.state not in ['sale', 'done'])
|
||||
unconfirmed_orders.invoice_status = 'no'
|
||||
confirmed_orders = self - unconfirmed_orders
|
||||
confirmed_orders = self.filtered(lambda so: so.state == 'sale')
|
||||
(self - confirmed_orders).invoice_status = 'no'
|
||||
if not confirmed_orders:
|
||||
return
|
||||
line_invoice_status_all = [
|
||||
@@ -540,7 +530,7 @@ class SaleOrder(models.Model):
|
||||
['order_id', 'invoice_status'])]
|
||||
for order in confirmed_orders:
|
||||
line_invoice_status = [d[1] for d in line_invoice_status_all if d[0] == order.id]
|
||||
if order.state not in ('sale', 'done'):
|
||||
if order.state != 'sale':
|
||||
order.invoice_status = 'no'
|
||||
elif any(invoice_status == 'to invoice' for invoice_status in line_invoice_status):
|
||||
order.invoice_status = 'to invoice'
|
||||
@@ -819,7 +809,7 @@ class SaleOrder(models.Model):
|
||||
:rtype: record of `mail.template` or `None` if not found
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.env.context.get('proforma') or self.state not in ('sale', 'done'):
|
||||
if self.env.context.get('proforma') or self.state != 'sale':
|
||||
return self.env.ref('sale.email_template_edi_sale', raise_if_not_found=False)
|
||||
else:
|
||||
return self._get_confirmation_template()
|
||||
@@ -845,7 +835,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
:raise: UserError if any given SO is not in draft state.
|
||||
"""
|
||||
if self.filtered(lambda so: so.state != 'draft'):
|
||||
if any(order.state != 'draft' for order in self):
|
||||
raise UserError(_("Only draft orders can be marked as sent directly."))
|
||||
|
||||
for order in self:
|
||||
@@ -860,12 +850,12 @@ class SaleOrder(models.Model):
|
||||
|
||||
:return: True
|
||||
:rtype: bool
|
||||
:raise: UserError if trying to confirm locked or cancelled SO's
|
||||
:raise: UserError if trying to confirm cancelled SO's
|
||||
"""
|
||||
if self._get_forbidden_state_confirm() & set(self.mapped('state')):
|
||||
if not all(order._can_be_confirmed() for order in self):
|
||||
raise UserError(_(
|
||||
"It is not allowed to confirm an order in the following states: %s",
|
||||
", ".join(self._get_forbidden_state_confirm()),
|
||||
"The following orders are not in a state requiring confirmation: %s",
|
||||
", ".join(self.mapped('display_name')),
|
||||
))
|
||||
|
||||
self.order_line._validate_analytic_distribution()
|
||||
@@ -885,12 +875,13 @@ class SaleOrder(models.Model):
|
||||
|
||||
self.with_context(context)._action_confirm()
|
||||
if self.env.user.has_group('sale.group_auto_done_setting'):
|
||||
self.action_done()
|
||||
self.action_lock()
|
||||
|
||||
return True
|
||||
|
||||
def _get_forbidden_state_confirm(self):
|
||||
return {'done', 'cancel'}
|
||||
def _can_be_confirmed(self):
|
||||
self.ensure_one()
|
||||
return self.state in {'draft', 'sent'}
|
||||
|
||||
def _prepare_confirmation_values(self):
|
||||
""" Prepare the sales order confirmation values.
|
||||
@@ -959,16 +950,16 @@ class SaleOrder(models.Model):
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
def action_done(self):
|
||||
def action_lock(self):
|
||||
for order in self:
|
||||
tx = order.sudo().transaction_ids._get_last()
|
||||
if tx and tx.state == 'pending' and tx.provider_id.code == 'custom':
|
||||
tx._set_done()
|
||||
tx.write({'is_post_processed': True})
|
||||
self.write({'state': 'done'})
|
||||
self.locked = True
|
||||
|
||||
def action_unlock(self):
|
||||
self.write({'state': 'sale'})
|
||||
self.locked = False
|
||||
|
||||
def action_cancel(self):
|
||||
""" Cancel SO after showing the cancel wizard when needed. (cfr :meth:`_show_cancel_wizard`)
|
||||
@@ -977,6 +968,8 @@ class SaleOrder(models.Model):
|
||||
|
||||
note: self.ensure_one() if the wizard is shown.
|
||||
"""
|
||||
if any(order.locked for order in self):
|
||||
raise UserError(_("You cannot cancel a locked order. Please unlock it first."))
|
||||
cancel_warning = self._show_cancel_wizard()
|
||||
if cancel_warning:
|
||||
self.ensure_one()
|
||||
@@ -1424,11 +1417,16 @@ class SaleOrder(models.Model):
|
||||
# PAYMENT #
|
||||
|
||||
def _force_lines_to_invoice_policy_order(self):
|
||||
"""Force the qty_to_invoice to be computed as if the invoice_policy
|
||||
was set to "Ordered quantities", independently of the product configuration.
|
||||
|
||||
This is needed for the automatic invoice logic, as we want to automatically
|
||||
invoice the full SO when it's paid.
|
||||
"""
|
||||
for line in self.order_line:
|
||||
if self.state in ['sale', 'done']:
|
||||
if line.state == 'sale':
|
||||
# No need to set 0 as it is already the standard logic in the compute method.
|
||||
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
||||
else:
|
||||
line.qty_to_invoice = 0
|
||||
|
||||
def payment_action_capture(self):
|
||||
""" Capture all transactions linked to this sale order. """
|
||||
@@ -1534,7 +1532,7 @@ class SaleOrder(models.Model):
|
||||
|
||||
def _get_report_base_filename(self):
|
||||
self.ensure_one()
|
||||
return '%s %s' % (self.type_name, self.name)
|
||||
return f'{self.type_name} {self.name}'
|
||||
|
||||
#=== CORE METHODS OVERRIDES ===#
|
||||
|
||||
@@ -1564,7 +1562,7 @@ class SaleOrder(models.Model):
|
||||
for order in self:
|
||||
name = order.name
|
||||
if order.partner_id.name:
|
||||
name = '%s - %s' % (name, order.partner_id.name)
|
||||
name = f'{name} - {order.partner_id.name}'
|
||||
res.append((order.id, name))
|
||||
return res
|
||||
return super().name_get()
|
||||
|
||||
@@ -764,7 +764,7 @@ class SaleOrderLine(models.Model):
|
||||
calculated from the ordered quantity. Otherwise, the quantity delivered is used.
|
||||
"""
|
||||
for line in self:
|
||||
if line.state in ['sale', 'done'] and not line.display_type:
|
||||
if line.state == 'sale' and not line.display_type:
|
||||
if line.product_id.invoice_policy == 'order':
|
||||
line.qty_to_invoice = line.product_uom_qty - line.qty_invoiced
|
||||
else:
|
||||
@@ -776,20 +776,19 @@ class SaleOrderLine(models.Model):
|
||||
def _compute_invoice_status(self):
|
||||
"""
|
||||
Compute the invoice status of a SO line. Possible statuses:
|
||||
- no: if the SO is not in status 'sale' or 'done', we consider that there is nothing to
|
||||
- no: if the SO is not in status 'sale', we consider that there is nothing to
|
||||
invoice. This is also the default value if the conditions of no other status is met.
|
||||
- to invoice: we refer to the quantity to invoice of the line. Refer to method
|
||||
`_compute_qty_to_invoice()` for more information on how this quantity is calculated.
|
||||
- upselling: this is possible only for a product invoiced on ordered quantities for which
|
||||
we delivered more than expected. The could arise if, for example, a project took more
|
||||
time than expected but we decided not to invoice the extra cost to the client. This
|
||||
occurs only in state 'sale', so that when a SO is set to done, the upselling opportunity
|
||||
is removed from the list.
|
||||
occurs only in state 'sale', the upselling opportunity is removed from the list.
|
||||
- invoiced: the quantity invoiced is larger or equal to the quantity ordered.
|
||||
"""
|
||||
precision = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
for line in self:
|
||||
if line.state not in ('sale', 'done'):
|
||||
if line.state != 'sale':
|
||||
line.invoice_status = 'no'
|
||||
elif line.is_downpayment and line.untaxed_amount_to_invoice == 0:
|
||||
line.invoice_status = 'invoiced'
|
||||
@@ -835,7 +834,7 @@ class SaleOrderLine(models.Model):
|
||||
"""
|
||||
for line in self:
|
||||
amount_to_invoice = 0.0
|
||||
if line.state in ['sale', 'done']:
|
||||
if line.state == 'sale':
|
||||
# Note: do not use price_subtotal field as it returns zero when the ordered quantity is
|
||||
# zero. It causes problem for expense line (e.i.: ordered qty = 0, deli qty = 4,
|
||||
# price_unit = 20 ; subtotal is zero), but when you can invoice the line, you see an
|
||||
@@ -888,7 +887,13 @@ class SaleOrderLine(models.Model):
|
||||
@api.depends('product_id', 'state', 'qty_invoiced', 'qty_delivered')
|
||||
def _compute_product_updatable(self):
|
||||
for line in self:
|
||||
if line.state in ['done', 'cancel'] or (line.state == 'sale' and (line.qty_invoiced > 0 or line.qty_delivered > 0)):
|
||||
if line.state == 'cancel':
|
||||
line.product_updatable = False
|
||||
elif line.state == 'sale' and (
|
||||
line.order_id.locked
|
||||
or line.qty_invoiced > 0
|
||||
or line.qty_delivered > 0
|
||||
):
|
||||
line.product_updatable = False
|
||||
else:
|
||||
line.product_updatable = True
|
||||
@@ -897,7 +902,7 @@ class SaleOrderLine(models.Model):
|
||||
def _compute_product_uom_readonly(self):
|
||||
for line in self:
|
||||
# line.ids checks whether it's a new record not yet saved
|
||||
line.product_uom_readonly = line.ids and line.state in ['sale', 'done', 'cancel']
|
||||
line.product_uom_readonly = line.ids and line.state in ['sale', 'cancel']
|
||||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@@ -967,7 +972,7 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
# Prevent writing on a locked SO.
|
||||
protected_fields = self._get_protected_fields()
|
||||
if 'done' in self.mapped('state') and any(f in values.keys() for f in protected_fields):
|
||||
if any(self.order_id.mapped('locked')) and any(f in values.keys() for f in protected_fields):
|
||||
protected_fields_modified = list(set(protected_fields) & set(values.keys()))
|
||||
|
||||
if 'name' in protected_fields_modified and all(self.mapped('is_downpayment')):
|
||||
@@ -1031,7 +1036,7 @@ class SaleOrderLine(models.Model):
|
||||
"""
|
||||
return self.filtered(
|
||||
lambda line:
|
||||
line.state in ('sale', 'done')
|
||||
line.state == 'sale'
|
||||
and (line.invoice_lines or not line.is_downpayment)
|
||||
and not line.display_type
|
||||
)
|
||||
@@ -1071,7 +1076,7 @@ class SaleOrderLine(models.Model):
|
||||
|
||||
def _expected_date(self):
|
||||
self.ensure_one()
|
||||
if self.state in ['sale', 'done'] and self.order_id.date_order:
|
||||
if self.state == 'sale' and self.order_id.date_order:
|
||||
order_date = self.order_id.date_order
|
||||
else:
|
||||
order_date = fields.Datetime.now()
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models, tools
|
||||
from odoo.addons.sale.models.sale_order import SALE_ORDER_STATE
|
||||
|
||||
|
||||
class SaleReport(models.Model):
|
||||
@@ -13,7 +14,7 @@ class SaleReport(models.Model):
|
||||
|
||||
@api.model
|
||||
def _get_done_states(self):
|
||||
return ['sale', 'done']
|
||||
return ['sale']
|
||||
|
||||
# sale.order fields
|
||||
name = fields.Char(string="Order Reference", readonly=True)
|
||||
@@ -23,14 +24,7 @@ class SaleReport(models.Model):
|
||||
pricelist_id = fields.Many2one(comodel_name='product.pricelist', readonly=True)
|
||||
team_id = fields.Many2one(comodel_name='crm.team', string="Sales Team", readonly=True)
|
||||
user_id = fields.Many2one(comodel_name='res.users', string="Salesperson", readonly=True)
|
||||
state = fields.Selection(
|
||||
selection=[
|
||||
('draft', 'Draft Quotation'),
|
||||
('sent', 'Quotation Sent'),
|
||||
('sale', 'Sales Order'),
|
||||
('done', 'Sales Done'),
|
||||
('cancel', 'Cancelled'),
|
||||
], string='Status', readonly=True)
|
||||
state = fields.Selection(selection=SALE_ORDER_STATE, string="Status", readonly=True)
|
||||
analytic_account_id = fields.Many2one(
|
||||
comodel_name='account.analytic.account', string="Analytic Account", readonly=True)
|
||||
invoice_status = fields.Selection(
|
||||
|
||||
@@ -238,7 +238,8 @@ class TestSalePayment(AccountPaymentCommon, SaleCommon, PaymentHttpCommon):
|
||||
with mute_logger('odoo.addons.sale.models.payment_transaction'):
|
||||
tx._reconcile_after_done()
|
||||
|
||||
self.assertEqual(self.sale_order.state, 'done')
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(self.sale_order.locked)
|
||||
self.assertTrue(tx.invoice_ids)
|
||||
self.assertTrue(self.sale_order.invoice_ids)
|
||||
self.assertTrue(tx.invoice_ids.is_move_sent)
|
||||
|
||||
@@ -160,14 +160,15 @@ class TestSaleOrder(SaleCommon):
|
||||
so_copy.with_user(self.sale_user).unlink()
|
||||
self.assertTrue(so_copy.unlink(), 'Sale: deleting a cancelled SO should be possible')
|
||||
|
||||
# SO in state 'sale' or 'done' cannot be deleted
|
||||
# SO in state 'sale' cannot be deleted
|
||||
self.sale_order.action_confirm()
|
||||
self.assertTrue(self.sale_order.state == 'sale', 'Sale: SO should be in state "sale"')
|
||||
with self.assertRaises(UserError):
|
||||
self.sale_order.unlink()
|
||||
|
||||
self.sale_order.action_done()
|
||||
self.assertTrue(self.sale_order.state == 'done', 'Sale: SO should be in state "done"')
|
||||
self.sale_order.action_lock()
|
||||
self.assertTrue(self.sale_order.state == 'sale')
|
||||
self.assertTrue(self.sale_order.locked)
|
||||
with self.assertRaises(UserError):
|
||||
self.sale_order.unlink()
|
||||
|
||||
@@ -312,7 +313,8 @@ class TestSaleOrder(SaleCommon):
|
||||
|
||||
self.env.user.groups_id += self.env.ref('sale.group_auto_done_setting')
|
||||
self.sale_order.action_confirm()
|
||||
self.assertEqual(self.sale_order.state, 'done', "The order wasn't automatically locked at confirmation.")
|
||||
self.assertEqual(self.sale_order.state, 'sale')
|
||||
self.assertTrue(self.sale_order.locked)
|
||||
with self.assertRaises(UserError):
|
||||
self.sale_order.action_confirm()
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
<div class="oe_kanban_bottom_right">
|
||||
<field name="state"
|
||||
widget="label_selection"
|
||||
options="{'classes': {'draft': 'info', 'cancel': 'default', 'done': 'success', 'sale': 'success'}}"/>
|
||||
options="{'classes': {'draft': 'info', 'cancel': 'default', 'sale': 'success'}}"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -113,9 +113,9 @@
|
||||
sample="1"
|
||||
decoration-muted="state == 'cancel'">
|
||||
<header>
|
||||
<button name="%(sale.action_view_sale_advance_payment_inv)d"
|
||||
type="action"
|
||||
string="Create Invoices"
|
||||
<button name="%(sale.action_view_sale_advance_payment_inv)d"
|
||||
type="action"
|
||||
string="Create Invoices"
|
||||
class="btn-secondary"/>
|
||||
</header>
|
||||
<field name="message_needaction" invisible="1"/>
|
||||
@@ -153,7 +153,7 @@
|
||||
options="{'color_field': 'color'}"
|
||||
optional="hide"/>
|
||||
<field name="state"
|
||||
decoration-success="state == 'sale' or state == 'done'"
|
||||
decoration-success="state == 'sale'"
|
||||
decoration-info="state == 'draft'"
|
||||
decoration-primary="state == 'sent'"
|
||||
widget="badge"
|
||||
@@ -226,6 +226,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<form string="Sales Order" class="o_sale_order">
|
||||
<header>
|
||||
<field name="locked" invisible="1"/>
|
||||
<field name="authorized_transaction_ids" invisible="1"/>
|
||||
<button name="payment_action_capture" type="object" data-hotkey="shift+g"
|
||||
string="Capture Transaction" class="oe_highlight"
|
||||
@@ -252,9 +253,19 @@
|
||||
attrs="{'invisible': [('state', 'not in', ['draft'])]}"/>
|
||||
<button name="action_quotation_send" type="object" string="Send PRO-FORMA Invoice" groups="sale.group_proforma_sales" attrs="{'invisible': ['|', ('state', '=', 'draft'), ('invoice_count','>=',1)]}" context="{'proforma': True, 'validate_analytic': True}"/>
|
||||
<button name="action_quotation_send" string="Send by Email" type="object" states="sent,sale" data-hotkey="g" context="{'validate_analytic': True}"/>
|
||||
|
||||
<!-- allow to unlock locked orders even if setting is not enabled (e.g. orders synchronized from connectors) -->
|
||||
<button name="action_unlock" type="object" string="Unlock"
|
||||
attrs="{'invisible': [('locked', '=', False)]}" groups="sales_team.group_sale_manager"/>
|
||||
<button name="action_preview_sale_order" string="Preview" type="object" class="btn-secondary"/>
|
||||
<button name="action_cancel" type="object" string="Cancel" attrs="{'invisible': ['|', ('state', 'not in', ['draft', 'sent','sale']), ('id', '=', False)]}" data-hotkey="z"/>
|
||||
<button name="action_draft" states="cancel" type="object" string="Set to Quotation" data-hotkey="w"/>
|
||||
<t groups="sale.group_auto_done_setting">
|
||||
<button name="action_lock" type="object" string="Lock"
|
||||
help="If the sale is locked, you can not modify it anymore. However, you will still be able to invoice or deliver."
|
||||
attrs="{'invisible': ['|', ('locked', '=', True), ('state', '!=', 'sale')]}"
|
||||
groups="sales_team.group_sale_manager"/>
|
||||
</t>
|
||||
<field name="state" widget="statusbar" statusbar_visible="draft,sent,sale"/>
|
||||
</header>
|
||||
<div
|
||||
@@ -272,6 +283,11 @@
|
||||
<field name="invoice_count" widget="statinfo" string="Invoices"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="badge rounded-pill text-bg-secondary float-end fs-6 border-0"
|
||||
attrs="{'invisible': [('locked', '=', False)]}">
|
||||
<i class="fa fa-lock"/>
|
||||
Locked
|
||||
</div>
|
||||
<div class="oe_title">
|
||||
<h1>
|
||||
<field name="name" readonly="1"/>
|
||||
@@ -279,20 +295,28 @@
|
||||
</div>
|
||||
<group name="sale_header">
|
||||
<group name="partner_details">
|
||||
<field name="partner_id"
|
||||
widget="res_partner_many2one"
|
||||
context="{'res_partner_search_mode': 'customer', 'show_address': 1, 'show_vat': True}"
|
||||
options="{'always_reload': True}"
|
||||
<field name="partner_id"
|
||||
widget="res_partner_many2one"
|
||||
context="{'res_partner_search_mode': 'customer', 'show_address': 1, 'show_vat': True}"
|
||||
options="{'always_reload': True}"
|
||||
placeholder="Type to find a customer..."/>
|
||||
<field name="partner_invoice_id" groups="account.group_delivery_invoice_address" context="{'default_type':'invoice'}" options='{"always_reload": True}'/>
|
||||
<field name="partner_shipping_id" groups="account.group_delivery_invoice_address" context="{'default_type':'delivery'}" options='{"always_reload": True}'/>
|
||||
<field name="partner_invoice_id"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
context="{'default_type':'invoice'}"
|
||||
options="{'always_reload': True}"
|
||||
attrs="{'readonly': ['|', ('state', '=', 'cancel'), ('locked', '=', True)]}"/>
|
||||
<field name="partner_shipping_id"
|
||||
groups="account.group_delivery_invoice_address"
|
||||
context="{'default_type':'delivery'}"
|
||||
options="{'always_reload': True}"
|
||||
attrs="{'readonly': ['|', ('state', '=', 'cancel'), ('locked', '=', True)]}"/>
|
||||
</group>
|
||||
<group name="order_details">
|
||||
<field name="validity_date" attrs="{'invisible': [('state', 'in', ['sale', 'done'])]}"/>
|
||||
<div class="o_td_label" groups="base.group_no_one" attrs="{'invisible': [('state', 'in', ['sale', 'done', 'cancel'])]}">
|
||||
<field name="validity_date" attrs="{'invisible': [('state', 'in', ['sale'])]}"/>
|
||||
<div class="o_td_label" groups="base.group_no_one" attrs="{'invisible': [('state', 'in', ['sale', 'cancel'])]}">
|
||||
<label for="date_order" string="Quotation Date"/>
|
||||
</div>
|
||||
<field name="date_order" nolabel="1" groups="base.group_no_one" attrs="{'invisible': [('state', 'in', ['sale', 'done', 'cancel'])]}"/>
|
||||
<field name="date_order" nolabel="1" groups="base.group_no_one" attrs="{'invisible': [('state', 'in', ['sale', 'cancel'])]}"/>
|
||||
<div class="o_td_label" attrs="{'invisible': [('state', 'in', ['draft', 'sent'])]}">
|
||||
<label for="date_order" string="Order Date"/>
|
||||
</div>
|
||||
@@ -311,7 +335,7 @@
|
||||
help="Recompute all prices based on this pricelist"
|
||||
class="btn-link mb-1 px-0" icon="fa-refresh"
|
||||
confirm="This will update all unit prices based on the currently set pricelist."
|
||||
attrs="{'invisible': ['|', ('show_update_pricelist', '=', False), ('state', 'in', ['sale', 'done', 'cancel'])]}"/>
|
||||
attrs="{'invisible': ['|', ('show_update_pricelist', '=', False), ('state', 'in', ['sale', 'cancel'])]}"/>
|
||||
</div>
|
||||
<field name="country_code" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
@@ -328,7 +352,7 @@
|
||||
name="order_line"
|
||||
widget="section_and_note_one2many"
|
||||
mode="tree,kanban"
|
||||
attrs="{'readonly': [('state', 'in', ('done','cancel'))]}"
|
||||
attrs="{'readonly': ['|', ('state', '=', 'cancel'), ('locked', '=', True)]}"
|
||||
>
|
||||
<form>
|
||||
<field name="display_type" invisible="1"/>
|
||||
@@ -376,22 +400,24 @@
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<label for="qty_delivered" string="Delivered" attrs="{'invisible': [('parent.state', 'not in', ['sale', 'done'])]}"/>
|
||||
<div name="delivered_qty" attrs="{'invisible': [('parent.state', 'not in', ['sale', 'done'])]}">
|
||||
<label for="qty_delivered" string="Delivered" attrs="{'invisible': [('parent.state', '!=', 'sale')]}"/>
|
||||
<div name="delivered_qty" attrs="{'invisible': [('parent.state', '!=', 'sale')]}">
|
||||
<field name="qty_delivered" attrs="{'readonly': [('qty_delivered_method', '!=', 'manual')]}"/>
|
||||
</div>
|
||||
<label for="qty_invoiced" string="Invoiced" attrs="{'invisible': [('parent.state', 'not in', ['sale', 'done'])]}"/>
|
||||
<div name="invoiced_qty" attrs="{'invisible': [('parent.state', 'not in', ['sale', 'done'])]}">
|
||||
<field name="qty_invoiced" attrs="{'invisible': [('parent.state', 'not in', ['sale', 'done'])]}"/>
|
||||
<label for="qty_invoiced" string="Invoiced" attrs="{'invisible': [('parent.state', '!=', 'sale')]}"/>
|
||||
<div name="invoiced_qty" attrs="{'invisible': [('parent.state', '!=', 'sale')]}">
|
||||
<field name="qty_invoiced"/>
|
||||
</div>
|
||||
<field name="product_packaging_id" attrs="{'invisible': [('product_id', '=', False)]}" context="{'default_product_id': product_id, 'tree_view_ref':'product.product_packaging_tree_view', 'form_view_ref':'product.product_packaging_form_view'}" groups="product.group_stock_packaging" />
|
||||
<field name="price_unit"/>
|
||||
<field name="tax_id" widget="many2many_tags" options="{'no_create': True}" context="{'search_view_ref': 'account.account_tax_view_search'}" domain="[('type_tax_use','=','sale'), ('company_id','=',parent.company_id), ('country_id', '=', parent.tax_country_id)]"
|
||||
attrs="{'readonly': [('qty_invoiced', '>', 0)]}"/>
|
||||
<label for="discount" groups="product.group_discount_per_so_line"/>
|
||||
<div name="discount" groups="product.group_discount_per_so_line">
|
||||
<field name="discount" class="oe_inline"/> %
|
||||
</div>
|
||||
<t groups="product.group_discount_per_so_line">
|
||||
<label for="discount"/>
|
||||
<div name="discount">
|
||||
<field name="discount" class="oe_inline"/> %
|
||||
</div>
|
||||
</t>
|
||||
<!--
|
||||
We need the sequence field to be here
|
||||
because we want to be able to overwrite the default sequence value in the JS
|
||||
@@ -506,7 +532,7 @@
|
||||
decoration-info="(not display_type and invoice_status == 'to invoice')" decoration-bf="(not display_type and invoice_status == 'to invoice')"
|
||||
string="Delivered"
|
||||
attrs="{
|
||||
'column_invisible': [('parent.state', 'not in', ['sale', 'done'])],
|
||||
'column_invisible': [('parent.state', '!=', 'sale')],
|
||||
'readonly': ['|', ('qty_delivered_method', '!=', 'manual'), ('is_downpayment', '=', True)]
|
||||
}"
|
||||
optional="show"
|
||||
@@ -516,7 +542,7 @@
|
||||
name="qty_invoiced"
|
||||
decoration-info="(not display_type and invoice_status == 'to invoice')" decoration-bf="(not display_type and invoice_status == 'to invoice')"
|
||||
string="Invoiced"
|
||||
attrs="{'column_invisible': [('parent.state', 'not in', ['sale', 'done'])]}"
|
||||
attrs="{'column_invisible': [('parent.state', '!=', 'sale')]}"
|
||||
optional="show"
|
||||
/>
|
||||
<field name="qty_to_invoice" invisible="1"/>
|
||||
@@ -645,6 +671,11 @@
|
||||
</templates>
|
||||
</kanban>
|
||||
</field>
|
||||
<div class="oe_right d-flex gap-1 mb-2 ms-1"
|
||||
attrs="{'invisible': ['|', ('state', '=', 'cancel'), ('locked', '=', True)]}"
|
||||
invisible="1"
|
||||
name="so_button_below_order_lines">
|
||||
</div>
|
||||
<group name="note_group" col="6" class="mt-2 mt-md-0">
|
||||
<group colspan="4">
|
||||
<field colspan="2" name="note" nolabel="1" placeholder="Terms and conditions..."/>
|
||||
@@ -682,18 +713,18 @@
|
||||
help="Recompute all taxes based on this fiscal position"
|
||||
class="btn-link mb-1 px-0" icon="fa-refresh"
|
||||
confirm="This will update all taxes based on the currently selected fiscal position."
|
||||
attrs="{'invisible': ['|', ('show_update_fpos', '=', False), ('state', 'in', ['sale', 'done','cancel'])]}"/>
|
||||
attrs="{'invisible': ['|', ('show_update_fpos', '=', False), ('state', 'in', ['sale', 'cancel'])]}"/>
|
||||
</div>
|
||||
<field name="partner_invoice_id" groups="!account.group_delivery_invoice_address" invisible="1"/>
|
||||
<field name="analytic_account_id" context="{'default_partner_id':partner_invoice_id, 'default_name':name}" attrs="{'readonly': [('invoice_count','!=',0),('state','=','sale')]}" groups="analytic.group_analytic_accounting" force_save="1"/>
|
||||
<field name="invoice_status" states="sale,done" groups="base.group_no_one"/>
|
||||
<field name="invoice_status" states="sale" groups="base.group_no_one"/>
|
||||
<!-- test_event_configurator -->
|
||||
<field name="invoice_status" invisible="1" groups="!base.group_no_one"/>
|
||||
</group>
|
||||
<group name="sale_shipping" string="Shipping">
|
||||
<label for="commitment_date" string="Delivery Date"/>
|
||||
<div name="commitment_date_div" class="o_row">
|
||||
<field name="commitment_date"/>
|
||||
<field name="commitment_date" attrs="{'readonly': ['|', ('state', '=', 'cancel'), ('locked', '=', True)]}"/>
|
||||
<span name="expected_date_span" class="text-muted">Expected: <field name="expected_date" class="oe_inline" widget="date"/></span>
|
||||
</div>
|
||||
</group>
|
||||
@@ -723,28 +754,6 @@
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sales_order_auto_done_setting" model="ir.ui.view">
|
||||
<field name="name">sale.order.form</field>
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_draft" position="after">
|
||||
<t groups="sale.group_auto_done_setting">
|
||||
<button name="action_done" type="object" string="Lock"
|
||||
states="sale"
|
||||
help="If the sale is locked, you can not modify it anymore. However, you will still be able to invoice or deliver." groups="sales_team.group_sale_manager"/>
|
||||
</t>
|
||||
</button>
|
||||
<button name="action_preview_sale_order" position="before">
|
||||
<t groups="sale.group_auto_done_setting">
|
||||
<button name="action_unlock" type="object" string="Unlock"
|
||||
states="done"
|
||||
groups="sales_team.group_sale_manager"/>
|
||||
</t>
|
||||
</button>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_sales_order_filter" model="ir.ui.view">
|
||||
<field name="name">sale.order.list.select</field>
|
||||
<field name="model">sale.order</field>
|
||||
@@ -790,8 +799,8 @@
|
||||
<separator/>
|
||||
<filter string="My Quotations" name="my_quotation" domain="[('user_id', '=', uid)]"/>
|
||||
<separator/>
|
||||
<filter string="Quotations" name="draft" domain="[('state','in',('draft', 'sent'))]"/>
|
||||
<filter string="Sales Orders" name="sales" domain="[('state','in',('sale','done'))]"/>
|
||||
<filter string="Quotations" name="draft" domain="[('state', 'in', ('draft', 'sent'))]"/>
|
||||
<filter string="Sales Orders" name="sales" domain="[('state', '=', 'sale')]"/>
|
||||
<separator/>
|
||||
<filter string="Create Date" name="filter_create_date" date="create_date"/>
|
||||
</filter>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<span class='d-none d-md-inline' t-field="order.date_order" t-options="{'time_only': True}"/>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span t-if="order.state == 'done'" class="badge rounded-pill text-bg-success">
|
||||
<span t-if="order.locked" class="badge rounded-pill text-bg-success">
|
||||
<i class="fa fa-fw fa-check" role="img" aria-label="Done" title="Done"/>Done
|
||||
</span>
|
||||
</td>
|
||||
@@ -302,7 +302,7 @@
|
||||
<div t-if="message == 'sign_ok'" class="alert alert-success alert-dismissible d-print-none" role="status">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<strong>Thank You!</strong><br/>
|
||||
<t t-if="message == 'sign_ok' and sale_order.state in ['sale', 'done']">
|
||||
<t t-if="message == 'sign_ok' and sale_order.state == 'sale'">
|
||||
Your order has been confirmed.
|
||||
</t>
|
||||
<t t-elif="message == 'sign_ok' and sale_order._has_to_be_paid()">
|
||||
@@ -394,7 +394,7 @@
|
||||
</div>
|
||||
<div class="row" id="so_date">
|
||||
<div class="mb-3 col-6">
|
||||
<t t-if="sale_order.state in ['sale', 'done', 'cancel']">
|
||||
<t t-if="sale_order.state in ['sale', 'cancel']">
|
||||
<strong>Order Date:</strong>
|
||||
</t>
|
||||
<t t-else="">
|
||||
|
||||
@@ -17,7 +17,7 @@ class CrmTeam(models.Model):
|
||||
if self._context.get('in_sales_app') and self.use_opportunities:
|
||||
return self.env["ir.actions.actions"]._for_xml_id("sale.action_order_report_so_salesteam")
|
||||
return super(CrmTeam,self).action_primary_channel_button()
|
||||
|
||||
|
||||
def _graph_get_model(self):
|
||||
if self.use_opportunities and self._context.get('in_sales_app') :
|
||||
return 'sale.report'
|
||||
@@ -37,8 +37,8 @@ class CrmTeam(models.Model):
|
||||
if self.use_opportunities and self._context.get('in_sales_app'):
|
||||
return ['', _('Sales: Untaxed Total')]
|
||||
return super(CrmTeam,self)._graph_title_and_key()
|
||||
|
||||
|
||||
def _extra_sql_conditions(self):
|
||||
if self.use_opportunities and self._context.get('in_sales_app'):
|
||||
return "AND state in ('sale', 'done')"
|
||||
return "AND state = 'sale'"
|
||||
return super(CrmTeam,self)._extra_sql_conditions()
|
||||
|
||||
@@ -80,10 +80,12 @@ class SaleOrder(models.Model):
|
||||
return res
|
||||
|
||||
def _action_cancel(self):
|
||||
previously_confirmed = self.filtered(lambda s: s.state in ('sale', 'done'))
|
||||
previously_confirmed = self.filtered(lambda s: s.state == 'sale')
|
||||
res = super()._action_cancel()
|
||||
# Add/remove the points to our coupons
|
||||
for coupon, changes in previously_confirmed.filtered(lambda s: s.state not in ('sale', 'done'))._get_point_changes().items():
|
||||
for coupon, changes in previously_confirmed.filtered(
|
||||
lambda s: s.state != 'sale'
|
||||
)._get_point_changes().items():
|
||||
coupon.points -= changes
|
||||
# Remove any rewards
|
||||
self.order_line.filtered(lambda l: l.is_reward_line).unlink()
|
||||
@@ -513,7 +515,7 @@ class SaleOrder(models.Model):
|
||||
Updates (or creates) an entry in coupon_point_ids for the given coupons.
|
||||
"""
|
||||
self.ensure_one()
|
||||
if self.state in ('sale', 'done'):
|
||||
if self.state == 'sale':
|
||||
for coupon, points in coupon_points.items():
|
||||
coupon.sudo().points += points
|
||||
for pe in self.coupon_point_ids.sudo():
|
||||
|
||||
@@ -75,7 +75,7 @@ class SaleOrderLine(models.Model):
|
||||
res = super().create(vals_list)
|
||||
# Update our coupon points if the order is in a confirmed state
|
||||
for line in res:
|
||||
if line.coupon_id and line.points_cost and line.order_id.state in ('sale', 'done'):
|
||||
if line.coupon_id and line.points_cost and line.state == 'sale':
|
||||
line.coupon_id.points -= line.points_cost
|
||||
return res
|
||||
|
||||
@@ -87,7 +87,7 @@ class SaleOrderLine(models.Model):
|
||||
if cost_in_vals:
|
||||
# Update our coupon points if the order is in a confirmed state
|
||||
for line in self:
|
||||
if previous_cost[line] != line.points_cost and line.order_id.state in ('sale', 'done'):
|
||||
if previous_cost[line] != line.points_cost and line.state == 'sale':
|
||||
line.coupon_id.points += (previous_cost[line] - line.points_cost)
|
||||
return res
|
||||
|
||||
@@ -112,7 +112,7 @@ class SaleOrderLine(models.Model):
|
||||
line.order_id.code_enabled_rule_ids = line.order_id.code_enabled_rule_ids.filtered(lambda r: r.program_id != line.coupon_id.program_id)
|
||||
# Give back the points if the order is confirmed, points are given back if the order is cancelled but in this case we need to do it directly
|
||||
for line in related_lines:
|
||||
if line.order_id.state in ('sale', 'done'):
|
||||
if line.state == 'sale':
|
||||
line.coupon_id.points += line.points_cost
|
||||
res = super(SaleOrderLine, self | related_lines).unlink()
|
||||
coupons_to_unlink.sudo().unlink()
|
||||
|
||||
@@ -7,15 +7,20 @@
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="priority">10</field>
|
||||
<field name="arch" type="xml">
|
||||
<group name="note_group" position="before">
|
||||
<div class="oe_right d-flex gap-1 mb-2 ms-1">
|
||||
<button name="%(sale_loyalty.sale_loyalty_coupon_wizard_action)d" class="btn btn-secondary"
|
||||
string="Coupon Code" type="action" states="draft,sent,sale"/>
|
||||
<button name="action_open_reward_wizard" class="btn btn-secondary"
|
||||
string="Promotions" type="object" states="draft,sent,sale"
|
||||
help="Update current promotional lines and select new rewards if applicable."/>
|
||||
</div>
|
||||
</group>
|
||||
<div name="so_button_below_order_lines" position="attributes">
|
||||
<attribute name="invisible">0</attribute>
|
||||
</div>
|
||||
<div name="so_button_below_order_lines" position="inside">
|
||||
<button name="%(sale_loyalty.sale_loyalty_coupon_wizard_action)d"
|
||||
type="action"
|
||||
string="Coupon Code"
|
||||
class="btn btn-secondary"/>
|
||||
<button name="action_open_reward_wizard"
|
||||
type="object"
|
||||
string="Promotions"
|
||||
help="Update current promotional lines and select new rewards if applicable."
|
||||
class="btn btn-secondary"/>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ class MrpBom(models.Model):
|
||||
if not product_ids:
|
||||
return
|
||||
lines = self.env['sale.order.line'].sudo().search([
|
||||
('state', 'in', ('sale', 'done')),
|
||||
('state', '=', 'sale'),
|
||||
('invoice_status', 'in', ('no', 'to invoice')),
|
||||
('product_id', 'in', product_ids),
|
||||
('move_ids.state', '!=', 'cancel'),
|
||||
|
||||
@@ -1948,7 +1948,7 @@ class TestSaleMrpFlow(ValuationReconciliationTestCommon):
|
||||
wiz = Form(self.env[wiz_act['res_model']].with_context(wiz_act['context'])).save()
|
||||
wiz.process()
|
||||
|
||||
so.action_cancel()
|
||||
so._action_cancel()
|
||||
so.action_draft()
|
||||
so.action_confirm()
|
||||
self.assertEqual(len(so.picking_ids), 1, "The product was already delivered, no need to re-create a delivery order")
|
||||
|
||||
@@ -20,7 +20,7 @@ class Project(models.Model):
|
||||
sale_line_id = fields.Many2one(
|
||||
'sale.order.line', 'Sales Order Item', copy=False,
|
||||
compute="_compute_sale_line_id", store=True, readonly=False, index='btree_not_null',
|
||||
domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', '=?', partner_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', '=', 'sale'), ('order_partner_id', '=?', partner_id), '|', ('company_id', '=', False), ('company_id', '=', company_id)]",
|
||||
help="Sales order item that will be selected by default on the tasks and timesheets of this project,"
|
||||
" except if the employee set on the timesheets is explicitely linked to another sales order item on the project.\n"
|
||||
"It can be modified on each task and timesheet entry individually if necessary.")
|
||||
@@ -320,7 +320,7 @@ class Project(models.Model):
|
||||
domain = [
|
||||
('order_id', 'in', sale_items.order_id.ids),
|
||||
('is_downpayment', '=', False),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('display_type', '=', False),
|
||||
'|',
|
||||
'|',
|
||||
@@ -375,7 +375,7 @@ class Project(models.Model):
|
||||
[
|
||||
('product_id', '!=', False),
|
||||
('is_expense', '=', False),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
'|', ('qty_to_invoice', '>', 0), ('qty_invoiced', '>', 0),
|
||||
],
|
||||
domain,
|
||||
@@ -694,7 +694,7 @@ class ProjectTask(models.Model):
|
||||
'sale.order.line', 'Sales Order Item',
|
||||
copy=True, tracking=True, index='btree_not_null', recursive=True,
|
||||
compute='_compute_sale_line', store=True, readonly=False,
|
||||
domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', '=?', partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]",
|
||||
domain="[('company_id', '=', company_id), ('is_service', '=', True), ('order_partner_id', '=?', partner_id), ('is_expense', '=', False), ('state', '=', 'sale')]",
|
||||
help="Sales Order Item to which the time spent on this task will be added in order to be invoiced to your customer.\n"
|
||||
"By default the sales order item set on the project will be selected. In the absence of one, the last prepaid sales order item that has time remaining will be used.\n"
|
||||
"Remove the sales order item in order to make this task non billable. You can also change or remove the sales order item of each timesheet entry individually.")
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<record id="sale_order_line_rule_project_manager" model="ir.rule">
|
||||
<field name="name">Project Manager Sales Orders Line</field>
|
||||
<field name="model_id" ref="sale.model_sale_order_line"/>
|
||||
<field name="domain_force">['&', '&', ('state', 'in', ['sale', 'done']), ('is_service', '=', True), '|', ('project_id','!=', False), ('task_id','!=', False)]</field>
|
||||
<field name="domain_force">[('state', '=', 'sale'), ('is_service', '=', True), '|', ('project_id','!=', False), ('task_id','!=', False)]</field>
|
||||
<field name="groups" eval="[(4, ref('project.group_project_manager'))]"/>
|
||||
<field name="perm_create" eval="0"/>
|
||||
<field name="perm_write" eval="0"/>
|
||||
|
||||
@@ -27,7 +27,7 @@ class SaleOrder(models.Model):
|
||||
warehouse_id = fields.Many2one(
|
||||
'stock.warehouse', string='Warehouse', required=True,
|
||||
compute='_compute_warehouse_id', store=True, readonly=False, precompute=True,
|
||||
states={'sale': [('readonly', True)], 'done': [('readonly', True)], 'cancel': [('readonly', False)]},
|
||||
states={'sale': [('readonly', True)], 'cancel': [('readonly', False)]},
|
||||
check_company=True)
|
||||
picking_ids = fields.One2many('stock.picking', 'sale_id', string='Transfers')
|
||||
delivery_count = fields.Integer(string='Delivery Orders', compute='_compute_picking_ids')
|
||||
|
||||
@@ -187,7 +187,7 @@ class SaleOrderLine(models.Model):
|
||||
lines._action_launch_stock_rule(previous_product_uom_qty)
|
||||
return res
|
||||
|
||||
@api.depends('order_id.state')
|
||||
@api.depends('order_id.locked') # Why the fuck does this depend on whether the SO is locked or not ???
|
||||
def _compute_invoice_status(self):
|
||||
def check_moves_state(moves):
|
||||
# All moves states are either 'done' or 'cancel', and there is at least one 'done'
|
||||
@@ -197,18 +197,21 @@ class SaleOrderLine(models.Model):
|
||||
return False
|
||||
at_least_one_done = at_least_one_done or move.state == 'done'
|
||||
return at_least_one_done
|
||||
super(SaleOrderLine, self)._compute_invoice_status()
|
||||
super()._compute_invoice_status()
|
||||
for line in self:
|
||||
# We handle the following specific situation: a physical product is partially delivered,
|
||||
# but we would like to set its invoice status to 'Fully Invoiced'. The use case is for
|
||||
# products sold by weight, where the delivered quantity rarely matches exactly the
|
||||
# quantity ordered.
|
||||
if line.order_id.state == 'done'\
|
||||
and line.invoice_status == 'no'\
|
||||
and line.product_id.type in ['consu', 'product']\
|
||||
and line.product_id.invoice_policy == 'delivery'\
|
||||
and line.move_ids \
|
||||
and check_moves_state(line.move_ids):
|
||||
if (
|
||||
line.state == 'sale'
|
||||
and line.order_id.locked # Why only for locked orders ???
|
||||
and line.invoice_status == 'no'
|
||||
and line.product_id.type in ['consu', 'product']
|
||||
and line.product_id.invoice_policy == 'delivery'
|
||||
and line.move_ids
|
||||
and check_moves_state(line.move_ids)
|
||||
):
|
||||
line.invoice_status = 'invoiced'
|
||||
|
||||
@api.depends('move_ids')
|
||||
@@ -305,7 +308,7 @@ class SaleOrderLine(models.Model):
|
||||
procurements = []
|
||||
for line in self:
|
||||
line = line.with_company(line.company_id)
|
||||
if line.state != 'sale' or not line.product_id.type in ('consu', 'product'):
|
||||
if line.state != 'sale' or line.order_id.locked or not line.product_id.type in ('consu', 'product'):
|
||||
continue
|
||||
qty = line._get_qty_procurement(previous_product_uom_qty)
|
||||
if float_compare(qty, line.product_uom_qty, precision_digits=precision) == 0:
|
||||
|
||||
@@ -244,59 +244,6 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon):
|
||||
self.assertAlmostEqual(self.inv_2.invoice_line_ids.sorted()[0].quantity, 2.0, msg='Sale Stock: refund quantity on the invoice should be 2.0 instead of "%s".' % self.inv_2.invoice_line_ids.sorted()[0].quantity)
|
||||
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" instead of "%s" after invoicing the return' % self.so.invoice_status)
|
||||
|
||||
def test_03_sale_stock_delivery_partial(self):
|
||||
"""
|
||||
Test a SO with a product invoiced on delivery. Deliver partially and invoice the SO, when
|
||||
the SO is set on 'done', the SO should be fully invoiced.
|
||||
"""
|
||||
# intial so
|
||||
self.product = self.company_data['product_delivery_no']
|
||||
so_vals = {
|
||||
'partner_id': self.partner_a.id,
|
||||
'partner_invoice_id': self.partner_a.id,
|
||||
'partner_shipping_id': self.partner_a.id,
|
||||
'order_line': [(0, 0, {
|
||||
'name': self.product.name,
|
||||
'product_id': self.product.id,
|
||||
'product_uom_qty': 5.0,
|
||||
'product_uom': self.product.uom_id.id,
|
||||
'price_unit': self.product.list_price})],
|
||||
'pricelist_id': self.company_data['default_pricelist'].id,
|
||||
}
|
||||
self.so = self.env['sale.order'].create(so_vals)
|
||||
|
||||
# confirm our standard so, check the picking
|
||||
self.so.action_confirm()
|
||||
self.assertTrue(self.so.picking_ids, 'Sale Stock: no picking created for "invoice on delivery" storable products')
|
||||
|
||||
# invoice in on delivery, nothing should be invoiced
|
||||
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "nothing to invoice"')
|
||||
|
||||
# deliver partially
|
||||
pick = self.so.picking_ids
|
||||
pick.move_ids.write({'quantity_done': 4})
|
||||
res_dict = pick.button_validate()
|
||||
wizard = Form(self.env[(res_dict.get('res_model'))].with_context(res_dict['context'])).save()
|
||||
wizard.process_cancel_backorder()
|
||||
|
||||
#Check Exception error is logged on SO
|
||||
activity = self.env['mail.activity'].search([('res_id', '=', self.so.id), ('res_model', '=', 'sale.order')])
|
||||
self.assertEqual(len(activity), 1, 'When no backorder is created for a partial delivery, a warning error should be logged in its origin SO')
|
||||
|
||||
# Check quantity delivered
|
||||
del_qty = sum(sol.qty_delivered for sol in self.so.order_line)
|
||||
self.assertEqual(del_qty, 4.0, 'Sale Stock: delivered quantity should be 4.0 after partial delivery')
|
||||
|
||||
# Check invoice
|
||||
self.assertEqual(self.so.invoice_status, 'to invoice', 'Sale Stock: so invoice_status should be "to invoice" before invoicing')
|
||||
self.inv_1 = self.so._create_invoices()
|
||||
self.assertEqual(self.so.invoice_status, 'no', 'Sale Stock: so invoice_status should be "no" after invoicing')
|
||||
self.assertEqual(len(self.inv_1), 1, 'Sale Stock: only one invoice should be created')
|
||||
self.assertEqual(self.inv_1.amount_untaxed, self.inv_1.amount_untaxed, 'Sale Stock: amount in SO and invoice should be the same')
|
||||
|
||||
self.so.action_done()
|
||||
self.assertEqual(self.so.invoice_status, 'invoiced', 'Sale Stock: so invoice_status should be "invoiced" when set to done')
|
||||
|
||||
def test_04_create_picking_update_saleorderline(self):
|
||||
"""
|
||||
Test that updating multiple sale order lines after a successful delivery creates a single picking containing
|
||||
@@ -1122,7 +1069,8 @@ class TestSaleStock(TestSaleCommon, ValuationReconciliationTestCommon):
|
||||
})],
|
||||
})
|
||||
so.action_confirm()
|
||||
self.assertEqual(so.state, 'done')
|
||||
self.assertEqual(so.state, 'sale')
|
||||
self.assertTrue(so.locked)
|
||||
so.picking_ids.action_cancel()
|
||||
|
||||
self.assertEqual(so.invoice_status, 'no')
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
<field name="effective_date" attrs="{'invisible': [('effective_date', '=', False)]}"/>
|
||||
</xpath>
|
||||
<field name="effective_date" position="after">
|
||||
<field name="delivery_status" attrs="{'invisible': [('state', 'not in', ['sale', 'done'])]}"/>
|
||||
<field name="delivery_status" attrs="{'invisible': [('state', '!=', 'sale')]}"/>
|
||||
</field>
|
||||
<xpath expr="//page[@name='other_information']//field[@name='expected_date']" position="after">
|
||||
<field name="show_json_popover" invisible="1"/>
|
||||
|
||||
@@ -30,7 +30,7 @@ class AccountAnalyticLine(models.Model):
|
||||
commercial_partner_id = fields.Many2one('res.partner', compute="_compute_commercial_partner")
|
||||
timesheet_invoice_id = fields.Many2one('account.move', string="Invoice", readonly=True, copy=False, help="Invoice created from the timesheet")
|
||||
so_line = fields.Many2one(compute="_compute_so_line", store=True, readonly=False,
|
||||
domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('order_partner_id', 'child_of', commercial_partner_id)]",
|
||||
domain="[('is_service', '=', True), ('is_expense', '=', False), ('state', '=', 'sale'), ('order_partner_id', 'child_of', commercial_partner_id)]",
|
||||
help="Sales order item to which the time spent will be added in order to be invoiced to your customer. Remove the sales order item for the timesheet entry to be non-billable.")
|
||||
# we needed to store it only in order to be able to groupby in the portal
|
||||
order_id = fields.Many2one(related='so_line.order_id', store=True, readonly=True, index=True)
|
||||
|
||||
@@ -186,7 +186,7 @@ class Project(models.Model):
|
||||
('is_service', '=', True),
|
||||
('order_partner_id', 'child_of', project.partner_id.commercial_partner_id.id),
|
||||
('is_expense', '=', False),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('remaining_hours', '>', 0)
|
||||
], limit=1)
|
||||
project.sale_line_id = sol or project.sale_line_employee_ids.sale_line_id[:1] # get the first SOL containing in the employee mappings if no sol found in the search
|
||||
@@ -590,7 +590,14 @@ class ProjectTask(models.Model):
|
||||
self.ensure_one()
|
||||
if not self.partner_id.commercial_partner_id or not self.allow_billable:
|
||||
return False
|
||||
domain = [('company_id', '=', self.company_id.id), ('is_service', '=', True), ('order_partner_id', 'child_of', self.partner_id.commercial_partner_id.id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done']), ('remaining_hours', '>', 0)]
|
||||
domain = [
|
||||
('company_id', '=', self.company_id.id),
|
||||
('is_service', '=', True),
|
||||
('order_partner_id', 'child_of', self.partner_id.commercial_partner_id.id),
|
||||
('is_expense', '=', False),
|
||||
('state', '=', 'sale'),
|
||||
('remaining_hours', '>', 0),
|
||||
]
|
||||
if self.project_id.pricing_type != 'task_rate' and self.project_sale_order_id and self.partner_id.commercial_partner_id == self.project_id.partner_id.commercial_partner_id:
|
||||
domain.append(('order_id', '=?', self.project_sale_order_id.id))
|
||||
return self.env['sale.order.line'].search(domain, limit=1)
|
||||
|
||||
@@ -17,7 +17,7 @@ class ProjectProductEmployeeMap(models.Model):
|
||||
domain="""[
|
||||
('is_service', '=', True),
|
||||
('is_expense', '=', False),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('order_partner_id', '=?', partner_id),
|
||||
'|', ('company_id', '=', False), ('company_id', '=', company_id)]""")
|
||||
sale_order_id = fields.Many2one(related="project_id.sale_order_id")
|
||||
|
||||
@@ -74,7 +74,7 @@ class SaleOrder(models.Model):
|
||||
def _get_order_with_valid_service_product(self):
|
||||
return self.env['sale.order.line']._read_group([
|
||||
('order_id', 'in', self.ids),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('is_service', '=', True),
|
||||
'|',
|
||||
('product_id.service_type', 'not in', ['milestones', 'manual']),
|
||||
|
||||
@@ -248,7 +248,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
||||
self.assertEqual(self.sale_order.tasks_count, 1, "The SO should still have only one task")
|
||||
self.assertEqual(so_line1.task_id.sale_line_id, so_line1, "The created task is also linked to its origin sale line, for invoicing purpose.")
|
||||
|
||||
self.sale_order.action_done()
|
||||
self.sale_order.action_lock()
|
||||
with self.assertRaises(UserError):
|
||||
so_line1.write({'product_uom_qty': 20})
|
||||
|
||||
@@ -512,7 +512,7 @@ class TestSaleService(TestCommonSaleTimesheet):
|
||||
sale_order_line.write({'product_uom_qty': 30})
|
||||
self.assertEqual(sale_order_line.product_uom_qty, sale_order_line.task_id.planned_hours, "The planned hours should have changed when updating the ordered quantity, even after SO cancellation")
|
||||
|
||||
self.sale_order.action_done()
|
||||
self.sale_order.action_lock()
|
||||
with self.assertRaises(UserError):
|
||||
sale_order_line.write({'product_uom_qty': 20})
|
||||
|
||||
|
||||
@@ -128,12 +128,12 @@
|
||||
<field name="so_line" groups="!sales_team.group_sale_salesman"
|
||||
attrs="{'column_invisible': [('parent.allow_billable', '=', False)], 'readonly': [('readonly_timesheet', '=', True)]}"
|
||||
context="{'with_remaining_hours': True, 'with_price_unit': True}" options="{'no_create': True, 'no_open': True}"
|
||||
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', parent.partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]"
|
||||
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', parent.partner_id), ('is_expense', '=', False), ('state', '=', 'sale')]"
|
||||
optional="hide"/>
|
||||
<field name="so_line" groups="sales_team.group_sale_salesman"
|
||||
attrs="{'column_invisible': [('parent.allow_billable', '=', False)], 'readonly': [('readonly_timesheet', '=', True)]}"
|
||||
context="{'with_remaining_hours': True, 'with_price_unit': True}" options="{'no_create': True, 'no_open': True}"
|
||||
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', parent.partner_id), ('is_expense', '=', False), ('state', 'in', ['sale', 'done'])]"
|
||||
domain="[('is_service', '=', True), ('order_partner_id', 'child_of', parent.partner_id), ('is_expense', '=', False), ('state', '=', 'sale')]"
|
||||
optional="hide"/>
|
||||
</xpath>
|
||||
<xpath expr="//field[@name='remaining_hours']" position="after">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
<template id="sale_order_portal_content_inherit" inherit_id="sale.sale_order_portal_template">
|
||||
<xpath expr="//li[.//a[@id='print_invoice_report']]" position="after">
|
||||
<li t-if="sale_order.timesheet_count > 0 and sale_order.state in ('sale', 'done')" class="list-group-item flex-grow-1">
|
||||
<li t-if="sale_order.timesheet_count > 0 and sale_order.state == 'sale'" class="list-group-item flex-grow-1">
|
||||
<div class="btn-toolbar flex-sm-nowrap justify-content-center">
|
||||
<a class="btn btn-primary w-100" t-att-href="'/my/timesheets?search_in=so&search=%s' % sale_order.name">View Timesheets</a>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@ class SaleOrder(models.Model):
|
||||
def set_delivery_line(self, carrier, amount):
|
||||
res = super().set_delivery_line(carrier, amount)
|
||||
for order in self:
|
||||
if order.state not in ('sale', 'done'):
|
||||
if order.state != 'sale':
|
||||
continue
|
||||
pending_deliveries = order.picking_ids.filtered(
|
||||
lambda p: p.state not in ('done', 'cancel')
|
||||
|
||||
@@ -47,7 +47,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
# Product-based computation
|
||||
sale_report_domain = [
|
||||
('website_id', '=', current_website.id),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('date', '>=', datetime_from),
|
||||
('date', '<=', fields.Datetime.now())
|
||||
]
|
||||
@@ -72,21 +72,21 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
for state, count in so_group_data:
|
||||
if state == 'sent':
|
||||
sales_values['summary']['order_unpaid_count'] += count
|
||||
elif state in ['sale', 'done']:
|
||||
elif state == 'sale':
|
||||
sales_values['summary']['order_count'] += count
|
||||
sales_values['summary']['order_carts_count'] += count
|
||||
|
||||
[price_subtotal] = request.env['sale.report']._read_group(
|
||||
domain=[
|
||||
('website_id', '=', current_website.id),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('date', '>=', datetime_from),
|
||||
('date', '<=', datetime_to)],
|
||||
aggregates=['price_subtotal:sum'],
|
||||
)[0]
|
||||
sales_values['summary'].update(
|
||||
order_to_invoice_count=request.env['sale.order'].search_count(sale_order_domain + [
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('order_line', '!=', False),
|
||||
('partner_id', '!=', request.env.ref('base.public_partner').id),
|
||||
('invoice_status', '=', 'to invoice'),
|
||||
@@ -129,7 +129,7 @@ class WebsiteSaleBackend(WebsiteBackend):
|
||||
def fetch_utm_data(self, date_from, date_to):
|
||||
sale_utm_domain = [
|
||||
('website_id', '!=', False),
|
||||
('state', 'in', ['sale', 'done']),
|
||||
('state', '=', 'sale'),
|
||||
('date_order', '>=', date_from),
|
||||
('date_order', '<=', date_to)
|
||||
]
|
||||
|
||||
@@ -83,7 +83,7 @@ class WebsiteSnippetFilter(models.Model):
|
||||
products = []
|
||||
sale_orders = self.env['sale.order'].sudo().search([
|
||||
('website_id', '=', website.id),
|
||||
('state', 'in', ('sale', 'done')),
|
||||
('state', '=', 'sale'),
|
||||
], limit=8, order='date_order DESC')
|
||||
if sale_orders:
|
||||
sold_products = [p.product_id.id for p in sale_orders.order_line]
|
||||
@@ -121,7 +121,7 @@ class WebsiteSnippetFilter(models.Model):
|
||||
current_id = int(current_id)
|
||||
sale_orders = self.env['sale.order'].sudo().search([
|
||||
('website_id', '=', website.id),
|
||||
('state', 'in', ('sale', 'done')),
|
||||
('state', '=', 'sale'),
|
||||
('order_line.product_id.product_tmpl_id', '=', current_id),
|
||||
], limit=8, order='date_order DESC')
|
||||
if sale_orders:
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<field name="partner_id"/>
|
||||
<field name="country_id"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<filter string="Confirmed Orders" name="confirmed" domain="[('state', 'in', ['sale', 'done'])]"/>
|
||||
<filter string="Confirmed Orders" name="confirmed" domain="[('state', '=', 'sale')]"/>
|
||||
<separator/>
|
||||
<filter name="filter_date" date="date" default_period="this_month"/>
|
||||
<group expand="0" string="Group By">
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<filter name="my_sale_orders_filter" position="before">
|
||||
<filter string="Confirmed" name="order_confirmed" domain="[('state', 'in', ('sale', 'done'))]"/>
|
||||
<filter string="Confirmed" name="order_confirmed" domain="[('state', '=', 'sale')]"/>
|
||||
<filter string="Unpaid" name="order_unpaid" domain="[('state', '=', 'sent')]"/>
|
||||
<filter string="Abandoned" name="order_abandoned" domain="[('is_abandoned_cart', '=', True)]"/>
|
||||
<separator/>
|
||||
@@ -125,7 +125,7 @@
|
||||
<field name="name">Orders To Invoice</field>
|
||||
<field name="res_model">sale.order</field>
|
||||
<field name="view_mode">tree,form,kanban</field>
|
||||
<field name="domain">[('state', 'in', ('sale', 'done')), ('order_line', '!=', False), ('invoice_status', '=', 'to invoice'), ('website_id', '!=', False)]</field>
|
||||
<field name="domain">[('state', '=', 'sale'), ('order_line', '!=', False), ('invoice_status', '=', 'to invoice'), ('website_id', '!=', False)]</field>
|
||||
<field name="context">{'show_sale': True, 'search_default_order_confirmed': 1, 'create': False}</field>
|
||||
<field name="search_view_id" ref="view_sales_order_filter_ecommerce"/>
|
||||
<field name="help" type="html">
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="website_id" eval="1"/>
|
||||
<field name="tag_ids" eval="[(4, ref('sales_team.categ_oppor2')), (4, ref('sales_team.categ_oppor5'))]"/>
|
||||
<field name="state">sale</field>
|
||||
</record>
|
||||
<record id="sale_order_course_1_line_1" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_course_1"/>
|
||||
@@ -34,7 +33,6 @@
|
||||
<field name="date_order" eval="DateTime.now() - relativedelta(days=100)"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="website_id" eval="1"/>
|
||||
<field name="state">sale</field>
|
||||
</record>
|
||||
<record id="sale_order_course_2_line_1" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_course_2"/>
|
||||
@@ -65,7 +63,6 @@
|
||||
<field name="date_order" eval="DateTime.now() - relativedelta(days=30)"/>
|
||||
<field name="team_id" ref="sales_team.salesteam_website_sales"/>
|
||||
<field name="website_id" eval="1"/>
|
||||
<field name="state">sale</field>
|
||||
</record>
|
||||
<record id="sale_order_course_3_line_1" model="sale.order.line">
|
||||
<field name="order_id" ref="sale_order_course_3"/>
|
||||
@@ -96,7 +93,10 @@
|
||||
</record>
|
||||
|
||||
<!-- Confirm sales -->
|
||||
<function model="sale.order" name="action_confirm" eval="[[ref('sale_order_course_1')]]"/>
|
||||
<function model="sale.order" name="action_confirm" eval="[[ref('sale_order_course_2')]]"/>
|
||||
<function model="sale.order" name="action_confirm" eval="[[ref('sale_order_course_3')]]"/>
|
||||
<function model="sale.order" name="action_confirm" eval="[[
|
||||
ref('sale_order_course_1'),
|
||||
ref('sale_order_course_2'),
|
||||
ref('sale_order_course_3'),
|
||||
]]"/>
|
||||
|
||||
</data></odoo>
|
||||
|
||||
Reference in New Issue
Block a user