sale_usability: track changes in delivered_qty in the chatter
This feature is native in the purchase module in v14. I consider that it replaces the 3 modules service_line_qty_update_base/service_line_qty_update_purchase/service_line_qty_update_sale
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
'views/account_move.xml',
|
||||
'views/res_company.xml',
|
||||
"views/res_partner.xml",
|
||||
"views/sale_template.xml",
|
||||
'wizards/sale_invoice_discount_all_lines_view.xml',
|
||||
'security/ir.model.access.csv',
|
||||
],
|
||||
|
||||
@@ -135,3 +135,29 @@ class SaleOrderLine(models.Model):
|
||||
if no_product_code_param and no_product_code_param == 'True':
|
||||
product = product.with_context(display_default_code=False)
|
||||
return super().get_sale_order_line_multiline_description_sale(product)
|
||||
|
||||
# In v12, I developped the 3 modules service_line_qty_update_base, service_line_qty_update_purchase
|
||||
# and service_line_qty_update_sale that add a wizard to update service lines and track the changes
|
||||
# in the chatter.
|
||||
# In v14, you can edit the quantity of the service lines directly and the purchase module
|
||||
# tracks changes in the chatter... but the sale module doesn't track the changes of 'qty_delivered'
|
||||
# So I "ported" that native feature of the purchase module to sale.order.line... here it is !
|
||||
# We can remove that code if this feature is added in the sale module (it's NOT the case in
|
||||
# odoo v17)
|
||||
def write(self, vals):
|
||||
if 'qty_delivered' in vals:
|
||||
for line in self:
|
||||
line._track_qty_delivered(vals['qty_delivered'])
|
||||
return super().write(vals)
|
||||
|
||||
def _track_qty_delivered(self, new_qty):
|
||||
self.ensure_one()
|
||||
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if (
|
||||
float_compare(new_qty, self.qty_delivered, precision_digits=prec) and
|
||||
self.order_id.state == 'sale'):
|
||||
self.order_id.message_post_with_view(
|
||||
'sale_usability.track_so_line_qty_delivered_template',
|
||||
values={'line': self, 'qty_delivered': new_qty},
|
||||
subtype_id=self.env.ref('mail.mt_note').id
|
||||
)
|
||||
|
||||
16
sale_usability/views/sale_template.xml
Normal file
16
sale_usability/views/sale_template.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
|
||||
<template id="track_so_line_qty_delivered_template">
|
||||
<div>
|
||||
<strong>The delivered quantity has been updated.</strong>
|
||||
<ul>
|
||||
<li><t t-esc="line.name"/>:</li>
|
||||
Delivered Quantity: <t t-esc="line.qty_delivered" /> -> <t t-esc="float(qty_delivered)"/><br/>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import wizard
|
||||
@@ -1,18 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Service Line Qty Update Base',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Update delivery qty on service lines - Base module',
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': ['product'],
|
||||
'data': [
|
||||
'wizard/service_qty_update_view.xml',
|
||||
],
|
||||
'installable': False,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import service_qty_update
|
||||
@@ -1,70 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com/)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import float_compare, float_is_zero
|
||||
import odoo.addons.decimal_precision as dp
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ServiceQtyUpdate(models.TransientModel):
|
||||
_name = 'service.qty.update'
|
||||
_description = 'Wizard to update delivery qty on service lines'
|
||||
|
||||
line_ids = fields.One2many('service.qty.update.line', 'parent_id', string="Lines")
|
||||
|
||||
def run(self):
|
||||
self.ensure_one()
|
||||
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
for line in self.line_ids:
|
||||
if float_compare(line.post_delivered_qty, line.order_qty, precision_digits=prec) > 0:
|
||||
raise UserError(_(
|
||||
"On line '%s', the total delivered qty (%s) is superior to the ordered qty (%s).") % (line.name, line.post_delivered_qty, line.order_qty))
|
||||
fc_added = float_compare(line.added_delivered_qty, 0, precision_digits=prec)
|
||||
if fc_added < 0:
|
||||
raise UserError(_(
|
||||
"On line '%s', the added quantity is negative.") % line.name)
|
||||
if fc_added > 0:
|
||||
line.process_line()
|
||||
return True
|
||||
|
||||
|
||||
class ServiceQtyUpdateLine(models.TransientModel):
|
||||
_name = 'service.qty.update.line'
|
||||
_description = 'Lines of the wizard that updates delivery qty on service lines'
|
||||
|
||||
parent_id = fields.Many2one(
|
||||
'service.qty.update', string='Wizard', ondelete='cascade')
|
||||
product_id = fields.Many2one('product.product', string='Product', readonly=True)
|
||||
name = fields.Char()
|
||||
name_readonly = fields.Char(related='name', string='Description')
|
||||
order_qty = fields.Float(
|
||||
string='Order Qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
order_qty_readonly = fields.Float(related='order_qty', string='Product Unit of Measure')
|
||||
pre_delivered_qty = fields.Float(
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
pre_delivered_qty_readonly = fields.Float(related='pre_delivered_qty', string='Current Delivered Qty')
|
||||
added_delivered_qty = fields.Float(
|
||||
string='Added Delivered Qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
post_delivered_qty = fields.Float(
|
||||
compute='_compute_post_delivered_qty',
|
||||
string='Total Delivered Qty',
|
||||
digits=dp.get_precision('Product Unit of Measure'))
|
||||
uom_id = fields.Many2one('uom.uom', string='UoM', readonly=True)
|
||||
comment = fields.Char(string='Comment')
|
||||
|
||||
@api.depends('pre_delivered_qty', 'added_delivered_qty')
|
||||
def _compute_post_delivered_qty(self):
|
||||
for line in self:
|
||||
line.post_delivered_qty = line.pre_delivered_qty + line.added_delivered_qty
|
||||
|
||||
def process_line(self):
|
||||
# Write and message_post
|
||||
return
|
||||
|
||||
# sale : qty_delivered
|
||||
# purchase : qty_received
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2020 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="service_qty_update_form" model="ir.ui.view">
|
||||
<field name="model">service.qty.update</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group name="main">
|
||||
<field name="line_ids" nolabel="1">
|
||||
<tree editable="bottom">
|
||||
<field name="product_id"/>
|
||||
<field name="name" invisible="0"/>
|
||||
<field name="name_readonly"/>
|
||||
<field name="order_qty" invisible="1"/>
|
||||
<field name="order_qty_readonly"/>
|
||||
<field name="pre_delivered_qty" invisible="1"/>
|
||||
<field name="pre_delivered_qty_readonly"/>
|
||||
<field name="added_delivered_qty"/>
|
||||
<field name="post_delivered_qty"/>
|
||||
<field name="uom_id" groups="uom.group_uom"/>
|
||||
<field name="comment"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="run" type="object" string="Validate" class="btn-primary"/>
|
||||
<button special="cancel" string="Cancel" class="btn-default"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="service_qty_update_action" model="ir.actions.act_window">
|
||||
<field name="name">Service Order Lines - Update Delivered Qty</field>
|
||||
<field name="res_model">service.qty.update</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -1,22 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Service Line Qty Update Purchase',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Update delivery qty on service lines - Purchase module',
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': [
|
||||
'purchase',
|
||||
'service_line_qty_update_base',
|
||||
'purchase_reception_status',
|
||||
],
|
||||
'data': [
|
||||
'views/purchase_order.xml',
|
||||
],
|
||||
'installable': False,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import purchase_order
|
||||
@@ -1,21 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class PurchaseOrder(models.Model):
|
||||
_inherit = 'purchase.order'
|
||||
|
||||
has_service = fields.Boolean(compute='_compute_has_service')
|
||||
|
||||
@api.depends('order_line.product_id.type')
|
||||
def _compute_has_service(self):
|
||||
for order in self:
|
||||
has_service = False
|
||||
for l in order.order_line:
|
||||
if l.product_id.type == 'service':
|
||||
has_service = True
|
||||
break
|
||||
order.has_service = has_service
|
||||
@@ -1,27 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2020 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="purchase_order_form" model="ir.ui.view">
|
||||
<field name="name">purchase.order.form</field>
|
||||
<field name="model">purchase.order</field>
|
||||
<field name="inherit_id" ref="purchase.purchase_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_view_invoice" position="after">
|
||||
<button name="%(service_line_qty_update_base.service_qty_update_action)d" type="action" string="Update Service Qty" attrs="{'invisible': ['|', '|', ('state', 'not in', ('purchase', 'done')), ('has_service', '=', False), ('reception_status', '=', 'received')]}" groups="purchase.group_purchase_user"/>
|
||||
<field name="has_service" invisible="1"/>
|
||||
</button>
|
||||
<xpath expr="//field[@name='order_line']/tree/field[@name='qty_received']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import service_qty_update
|
||||
@@ -1,62 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com/)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import float_compare
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ServiceQtyUpdate(models.TransientModel):
|
||||
_inherit = 'service.qty.update'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if self._context.get('active_model') == 'purchase.order' and self._context.get('active_id'):
|
||||
lines = []
|
||||
order = self.env['purchase.order'].browse(self._context['active_id'])
|
||||
for l in order.order_line.filtered(lambda x: x.product_id.type == 'service'):
|
||||
if float_compare(l.product_qty, l.qty_received, precision_digits=prec) > 0:
|
||||
lines.append((0, 0, {
|
||||
'purchase_line_id': l.id,
|
||||
'product_id': l.product_id.id,
|
||||
'name': l.name,
|
||||
'name_readonly': l.name,
|
||||
'order_qty': l.product_qty,
|
||||
'order_qty_readonly': l.product_qty,
|
||||
'pre_delivered_qty': l.qty_received,
|
||||
'pre_delivered_qty_readonly': l.qty_received,
|
||||
'uom_id': l.product_uom.id,
|
||||
}))
|
||||
if lines:
|
||||
res['line_ids'] = lines
|
||||
else:
|
||||
raise UserError(_(
|
||||
"All service lines are fully received."))
|
||||
return res
|
||||
|
||||
|
||||
class ServiceQtyUpdateLine(models.TransientModel):
|
||||
_inherit = 'service.qty.update.line'
|
||||
|
||||
purchase_line_id = fields.Many2one('purchase.order.line', string='Purchase Line', readonly=True)
|
||||
|
||||
def process_line(self):
|
||||
po_line = self.purchase_line_id
|
||||
if po_line:
|
||||
new_qty = po_line.qty_received + self.added_delivered_qty
|
||||
po_line.write({'qty_received': new_qty})
|
||||
body = """
|
||||
<p>Received qty updated on service line <b>%s</b>:
|
||||
<ul>
|
||||
<li>Added received qty: <b>%s</b></li>
|
||||
<li>Total received qty: %s</li>
|
||||
</ul></p>
|
||||
""" % (self.name, self.added_delivered_qty, new_qty)
|
||||
if self.comment:
|
||||
body += '<p>Comment: %s</p>' % self.comment
|
||||
po_line.order_id.message_post(body=body)
|
||||
return super().process_line()
|
||||
@@ -1,2 +0,0 @@
|
||||
from . import models
|
||||
from . import wizard
|
||||
@@ -1,22 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
{
|
||||
'name': 'Service Line Qty Update Sale',
|
||||
'version': '12.0.1.0.0',
|
||||
'category': 'Tools',
|
||||
'license': 'AGPL-3',
|
||||
'summary': 'Update delivery qty on service lines - Sale module',
|
||||
'author': 'Akretion',
|
||||
'website': 'http://www.akretion.com',
|
||||
'depends': [
|
||||
'sale',
|
||||
'service_line_qty_update_base',
|
||||
# 'purchase_reception_status',
|
||||
],
|
||||
'data': [
|
||||
'views/sale_order.xml',
|
||||
],
|
||||
'installable': False,
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
from . import sale_order
|
||||
@@ -1,21 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com)
|
||||
# @author Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_inherit = 'sale.order'
|
||||
|
||||
has_service = fields.Boolean(compute='_compute_has_service')
|
||||
|
||||
@api.depends('order_line.product_id.type')
|
||||
def _compute_has_service(self):
|
||||
for order in self:
|
||||
has_service = False
|
||||
for l in order.order_line:
|
||||
if l.product_id.type == 'service':
|
||||
has_service = True
|
||||
break
|
||||
order.has_service = has_service
|
||||
@@ -1,26 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright 2020 Akretion France (http://www.akretion.com/)
|
||||
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
-->
|
||||
|
||||
<odoo>
|
||||
|
||||
|
||||
<record id="view_order_form" model="ir.ui.view">
|
||||
<field name="model">sale.order</field>
|
||||
<field name="inherit_id" ref="sale.view_order_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<button name="action_quotation_send" position="after">
|
||||
<button name="%(service_line_qty_update_base.service_qty_update_action)d" type="action" string="Update Service Qty" attrs="{'invisible': ['|', ('state', 'not in', ('sale', 'done')), ('has_service', '=', False)]}" groups="sales_team.group_sale_salesman"/>
|
||||
<field name="has_service" invisible="1"/>
|
||||
</button>
|
||||
<xpath expr="//field[@name='order_line']/tree/field[@name='qty_delivered']" position="attributes">
|
||||
<attribute name="readonly">1</attribute>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
|
||||
</odoo>
|
||||
@@ -1 +0,0 @@
|
||||
from . import service_qty_update
|
||||
@@ -1,62 +0,0 @@
|
||||
# Copyright 2020 Akretion France (http://www.akretion.com/)
|
||||
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.tools import float_compare
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ServiceQtyUpdate(models.TransientModel):
|
||||
_inherit = 'service.qty.update'
|
||||
|
||||
@api.model
|
||||
def default_get(self, fields_list):
|
||||
res = super().default_get(fields_list)
|
||||
prec = self.env['decimal.precision'].precision_get('Product Unit of Measure')
|
||||
if self._context.get('active_model') == 'sale.order' and self._context.get('active_id'):
|
||||
lines = []
|
||||
order = self.env['sale.order'].browse(self._context['active_id'])
|
||||
for l in order.order_line.filtered(lambda x: x.product_id.type == 'service'):
|
||||
if float_compare(l.product_qty, l.qty_delivered, precision_digits=prec) > 0:
|
||||
lines.append((0, 0, {
|
||||
'sale_line_id': l.id,
|
||||
'product_id': l.product_id.id,
|
||||
'name': l.name,
|
||||
'name_readonly': l.name,
|
||||
'order_qty': l.product_uom_qty,
|
||||
'order_qty_readonly': l.product_uom_qty,
|
||||
'pre_delivered_qty': l.qty_delivered,
|
||||
'pre_delivered_qty_readonly': l.qty_delivered,
|
||||
'uom_id': l.product_uom.id,
|
||||
}))
|
||||
if lines:
|
||||
res['line_ids'] = lines
|
||||
else:
|
||||
raise UserError(_(
|
||||
"All service lines are fully delivered."))
|
||||
return res
|
||||
|
||||
|
||||
class ServiceQtyUpdateLine(models.TransientModel):
|
||||
_inherit = 'service.qty.update.line'
|
||||
|
||||
sale_line_id = fields.Many2one('sale.order.line', string='Sale Line', readonly=True)
|
||||
|
||||
def process_line(self):
|
||||
so_line = self.sale_line_id
|
||||
if so_line:
|
||||
new_qty = so_line.qty_delivered + self.added_delivered_qty
|
||||
so_line.write({'qty_delivered': new_qty})
|
||||
body = """
|
||||
<p>Delivered qty updated on service line <b>%s</b>:
|
||||
<ul>
|
||||
<li>Added delivered qty: <b>%s</b></li>
|
||||
<li>Total delivered qty: %s</li>
|
||||
</ul></p>
|
||||
""" % (self.name, self.added_delivered_qty, new_qty)
|
||||
if self.comment:
|
||||
body += '<p>Comment: %s</p>' % self.comment
|
||||
so_line.order_id.message_post(body=body)
|
||||
return super().process_line()
|
||||
Reference in New Issue
Block a user