[REF] repair, *_repair: Separate Repair from Sales/Invoicing scopes

Main changes include :
 - Separate Repair and Sales/Invoicing functionalities
 - Possible binding between a Sale Order and many Repair Orders
 - Specific 'repair.line' model is now replaced by a 'stock.move' inheritance
 - Added a new 'Repairs' Operation Type to warehouse(s)

Task : 3046560

Enterprise : odoo/enterprise#36088
Upgrade : odoo/upgrade#4391

closes odoo/odoo#106911

Signed-off-by: Arnold Moyaux (arm) <arm@odoo.com>
This commit is contained in:
Mathias Mathy (MAMA)
2023-07-20 17:01:00 +02:00
parent f11e84b10c
commit eaa6e1a08c
38 changed files with 1704 additions and 1571 deletions
+7 -18
View File
@@ -20,7 +20,7 @@ class Repair(models.Model):
def action_explode(self):
lines_to_unlink_ids = set()
line_vals_list = []
for op in self.operations:
for op in self.move_ids:
bom = self.env['mrp.bom'].sudo()._bom_find(op.product_id, company_id=op.company_id.id, bom_type='phantom')[op.product_id]
if not bom:
continue
@@ -31,35 +31,24 @@ class Repair(models.Model):
line_vals_list.append(op._prepare_phantom_line_vals(bom_line, line_data['qty']))
lines_to_unlink_ids.add(op.id)
self.env['repair.line'].browse(lines_to_unlink_ids).sudo().unlink()
self.env['stock.move'].browse(lines_to_unlink_ids).sudo().unlink()
if line_vals_list:
self.env['repair.line'].create(line_vals_list)
self.env['stock.move'].create(line_vals_list)
class RepairLine(models.Model):
_inherit = 'repair.line'
class StockMove(models.Model):
_inherit = 'stock.move'
def _prepare_phantom_line_vals(self, bom_line, qty):
self.ensure_one()
product = bom_line.product_id
uom = bom_line.product_uom_id
partner = self.repair_id.partner_id
price = self.repair_id.pricelist_id._get_product_price(product, qty, uom=uom)
tax = self.env['account.tax']
if partner:
partner_invoice = self.repair_id.partner_invoice_id or partner
fpos = self.env['account.fiscal.position']._get_fiscal_position(partner_invoice, delivery=self.repair_id.address_id)
taxes = self.product_id.taxes_id.filtered(lambda x: x.company_id == self.repair_id.company_id)
tax = fpos.map_tax(taxes)
return {
'name': self.name,
'repair_id': self.repair_id.id,
'type': self.type,
'repair_line_type': self.repair_line_type,
'product_id': product.id,
'price_unit': price,
'tax_id': [(4, t.id) for t in tax],
'price_unit': self.price_unit,
'product_uom_qty': qty,
'product_uom': uom.id,
'location_id': self.location_id.id,
'location_dest_id': self.location_dest_id.id,
'state': 'draft',
+6 -8
View File
@@ -49,12 +49,12 @@ class TestRepairTraceability(TestMrpCommon):
with Form(self.env['repair.order']) as ro_form:
ro_form.product_id = product_to_repair
ro_form.lot_id = ptrepair_lot # Repair product Serial A1
with ro_form.operations.new() as operation:
operation.type = 'remove'
with ro_form.move_ids.new() as operation:
operation.repair_line_type = 'remove'
operation.product_id = product_to_remove
operation.lot_id = ptremove_lot # Remove product Serial B2 from the product
ro = ro_form.save()
ro.action_validate()
ro.move_ids[0].lot_ids = ptremove_lot # Remove product Serial B2 from the product.
ro.action_repair_start()
ro.action_repair_end()
@@ -119,16 +119,14 @@ class TestRepairTraceability(TestMrpCommon):
mo = produce_one(finished, component)
self.assertEqual(mo.state, 'done')
self.assertEqual(mo.move_raw_ids.lot_ids, sn_lot)
ro_form = Form(self.env['repair.order'])
ro_form.product_id = finished
with ro_form.operations.new() as ro_line:
ro_line.type = 'remove'
with ro_form.move_ids.new() as ro_line:
ro_line.repair_line_type = 'recycle'
ro_line.product_id = component
ro_line.lot_id = sn_lot
ro_line.location_dest_id = stock_location
ro = ro_form.save()
ro.action_validate()
ro.move_ids[0].lot_ids = sn_lot
ro.action_repair_start()
ro.action_repair_end()
@@ -1,2 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_subcontracting_portal_repair_line,subcontracting.portal.repair_line,repair.model_repair_line,base.group_portal,1,0,0,0
access_subcontracting_portal_repair_move,subcontracting.portal.repair_move,repair.model_stock_move,base.group_portal,1,0,0,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_subcontracting_portal_repair_line access_subcontracting_portal_repair_move subcontracting.portal.repair_line subcontracting.portal.repair_move repair.model_repair_line repair.model_stock_move base.group_portal 1 0 0 0
@@ -3,7 +3,7 @@
<record id="repair_line_subcontracting_rule" model="ir.rule">
<field name="name">Repair Line Subcontractor</field>
<field name="model_id" ref="repair.model_repair_line"/>
<field name="model_id" ref="repair.model_stock_move"/>
<field name="domain_force">[
'|',
'|',
+13
View File
@@ -3,3 +3,16 @@
from . import models
from . import wizard
from . import report
from odoo import api, SUPERUSER_ID
def _create_warehouse_data(env):
""" This hook is used to add default repair picking types on every warehouse.
It is necessary if the repair module is installed after some warehouses were already created.
"""
warehouses = env['stock.warehouse'].search([('repair_type_id', '=', False)])
for warehouse in warehouses:
picking_type_vals = warehouse._create_or_update_sequences_and_picking_types()
if picking_type_vals:
warehouse.write(picking_type_vals)
+7 -5
View File
@@ -16,27 +16,29 @@ The following topics are covered by this module:
------------------------------------------------------
* Add/remove products in the reparation
* Impact for stocks
* Invoicing (products and/or services)
* Warranty concept
* Repair quotation report
* Notes for the technician and for the final customer
""",
'depends': ['stock', 'sale_management', 'account'],
'depends': ['stock', 'sale_management'],
'data': [
'security/ir.model.access.csv',
'security/repair_security.xml',
'wizard/repair_make_invoice_views.xml',
'wizard/stock_warn_insufficient_qty_views.xml',
'wizard/repair_warn_uncomplete_move.xml',
'views/product_views.xml',
'views/stock_move_views.xml',
'views/repair_views.xml',
'views/sale_order_views.xml',
'views/stock_lot_views.xml',
'views/stock_picking_views.xml',
'views/stock_warehouse_views.xml',
'report/repair_reports.xml',
'report/repair_templates_repair_order.xml',
'data/ir_sequence_data.xml',
'data/mail_template_data.xml',
'data/repair_data.xml',
],
'demo': ['data/repair_demo.xml'],
'post_init_hook': '_create_warehouse_data',
'installable': True,
'application': True,
'license': 'LGPL-3',
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="seq_repair" model="ir.sequence">
<field name="name">Repair Order</field>
<field name="code">repair.order</field>
<field name="prefix">RO/</field>
<field name="padding">5</field>
<field name="company_id" eval="False"/>
</record>
</data>
</odoo>
-36
View File
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="mail_template_repair_quotation" model="mail.template">
<field name="name">Repair: Quotation</field>
<field name="model_id" ref="repair.model_repair_order"/>
<field name="subject">{{ object.partner_id.name }} Repair Orders (Ref {{ object.name or 'n/a' }})</field>
<field name="email_from">{{ (object.create_uid.email_formatted or user.email_formatted) }}</field>
<field name="partner_to">{{ object.partner_id.id }}</field>
<field name="description">Sent manually when clicking on "Send Quotation" on a repair order</field>
<field name="body_html" type="html">
<div style="margin: 0px; padding: 0px;">
<p style="margin: 0px; padding: 0px;font-size: 13px;">
Hello <t t-out="object.partner_id.name or ''">Brandon Freeman</t>,<br/>
Here is your repair order <strong t-out="object.name or ''">RO/00004</strong>
<t t-if="object.invoice_method != 'none'">
amounting in <strong><t t-out="format_amount(object.amount_total, object.currency_id) or ''">$ 100.00</t>.</strong><br/>
</t>
<t t-else="">
.<br/>
</t>
You can reply to this email if you have any questions.
<br/><br/>
Thank you,
<t t-if="user.signature">
<br />
<t t-out="user.signature or ''">--<br/>Mitchell Admin</t>
</t>
</p>
</div></field>
<field name="report_template_ids" eval="[(4, ref('repair.action_report_repair_order'))]"/>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<!--
Given that data are loaded before update of warehouses through the post_init_hook,
we manually create the 'Repairs' Operation Type in warehouse0 here such that it's available
when the initialization of data triggers the default method of the required field 'picking_type_id'.
-->
<record id="picking_type_warehouse0_repair" model="stock.picking.type" forcecreate="0">
<field name="name">Repairs</field>
<field name="code">repair_operation</field>
<field name="company_id" ref="base.main_company"/>
<field name="default_location_src_id" ref="stock.stock_location_stock"/>
<field name="default_location_dest_id" model="stock.location" search="[('usage', '=', 'production'), ('company_id', '=', obj().env.ref('base.main_company').id)]"/>
<field name="default_remove_location_dest_id" model="stock.location" search="[('scrap_location', '=', True), ('company_id', '=', obj().env.ref('base.main_company').id)]"/>
<field name="default_recycle_location_dest_id" ref="stock.stock_location_stock"/>
<field name="sequence_code">RO</field>
<field name="warehouse_id" ref="stock.warehouse0"/>
</record>
<record id='stock.warehouse0' model='stock.warehouse'>
<field name='repair_type_id' ref='repair.picking_type_warehouse0_repair'/>
</record>
</odoo>
+10 -48
View File
@@ -6,38 +6,26 @@
<field name="standard_price">20.5</field>
<field name="list_price">30.75</field>
<field name="type">service</field>
<field name="create_repair">True</field>
</record>
<record id="repair_r1" model="repair.order">
<field name="schedule_date" eval="DateTime.today()"/>
<field name="address_id" ref="base.res_partner_address_1"/>
<field name="guarantee_limit" eval="datetime.today().strftime('%Y-%m-%d')"/>
<field name="invoice_method">none</field>
<field name="user_id"/>
<field name="product_id" ref="product.product_product_3"/>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field name="partner_invoice_id" ref="base.res_partner_address_1"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="operations" model="repair.line" eval="[(5, 0, 0), (0, 0, {
<field name="picking_type_id" ref="picking_type_warehouse0_repair"/>
<field name="move_ids" model="stock.move" eval="[(5, 0, 0), (0, 0, {
'location_dest_id': obj().env.ref('product.product_product_11').property_stock_production.id,
'location_id': obj().env.ref('stock.stock_location_stock').id,
'name': obj().env.ref('product.product_product_11').get_product_multiline_description_sale(),
'product_id': obj().env.ref('product.product_product_11').id,
'product_uom': obj().env.ref('uom.product_uom_unit').id,
'product_uom_qty': '1.0',
'price_unit': 50.0,
'state': 'draft',
'type': 'add',
'repair_line_type': 'add',
'company_id': obj().env.ref('base.main_company').id,
})]"/>
<field name="fees_lines" model="repair.fee" eval="[(5, 0, 0), (0, 0, {
'name': obj().env.ref('repair.product_service_order_repair').get_product_multiline_description_sale(),
'product_id': obj().env.ref('repair.product_service_order_repair').id,
'product_uom_qty': 1.0,
'product_uom': obj().env.ref('uom.product_uom_unit').id,
'company_id': obj().env.ref('base.main_company').id,
'price_unit': 50.0,
})]"/>
<field name="partner_id" ref="base.res_partner_12"/>
</record>
@@ -45,30 +33,17 @@
<field name="schedule_date" eval="DateTime.today()"/>
<field name="product_id" ref="product.product_product_5"/>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field name="address_id" ref="base.res_partner_address_1"/>
<field name="guarantee_limit" eval="datetime.today().strftime('%Y-%m-%d')"/>
<field name="invoice_method">after_repair</field>
<field name="user_id"/>
<field name="partner_invoice_id" ref="base.res_partner_address_1"/>
<field name="location_id" ref="stock.stock_location_stock"/>
<field name="operations" model="repair.line" eval="[(5, 0, 0), (0, 0, {
<field name="picking_type_id" ref="picking_type_warehouse0_repair"/>
<field name="move_ids" model="stock.move" eval="[(5, 0, 0), (0, 0, {
'location_dest_id': obj().env.ref('product.product_product_12').property_stock_production.id,
'location_id': obj().env.ref('stock.stock_location_stock').id,
'name': obj().env.ref('product.product_product_12').get_product_multiline_description_sale(),
'price_unit': 50.0,
'product_id': obj().env.ref('product.product_product_12').id,
'product_uom': obj().env.ref('uom.product_uom_unit').id,
'product_uom_qty': 1.0,
'state': 'draft',
'type': 'add',
'company_id': obj().env.ref('base.main_company').id,
})]"/>
<field name="fees_lines" model="repair.fee" eval="[(5, 0, 0), (0, 0, {
'name': obj().env.ref('repair.product_service_order_repair').get_product_multiline_description_sale(),
'product_id': obj().env.ref('repair.product_service_order_repair').id,
'product_uom_qty': 1.0,
'product_uom': obj().env.ref('uom.product_uom_unit').id,
'price_unit': 50.0,
'repair_line_type': 'add',
'company_id': obj().env.ref('base.main_company').id,
})]"/>
<field name="partner_id" ref="base.res_partner_12"/>
@@ -79,30 +54,17 @@
<field name="schedule_date" eval="DateTime.today() + relativedelta(days=-5)"/>
<field name="product_id" ref="product.product_product_6"/>
<field name="product_uom" ref="uom.product_uom_unit"/>
<field name="address_id" ref="base.res_partner_address_1"/>
<field name="guarantee_limit" eval="datetime.today().strftime('%Y-%m-%d')"/>
<field name="invoice_method">b4repair</field>
<field name="user_id"/>
<field name="partner_invoice_id" ref="base.res_partner_address_1"/>
<field name="location_id" ref="stock.stock_location_14"/>
<field name="operations" model="repair.line" eval="[(5, 0, 0), (0, 0, {
<field name="picking_type_id" ref="picking_type_warehouse0_repair"/>
<field name="move_ids" model="stock.move" eval="[(5, 0, 0), (0, 0, {
'location_dest_id': obj().env.ref('product.product_product_13').property_stock_production.id,
'location_id': obj().env.ref('stock.stock_location_stock').id,
'name': obj().env.ref('product.product_product_13').get_product_multiline_description_sale(),
'price_unit': 50.0,
'product_id': obj().env.ref('product.product_product_13').id,
'product_uom': obj().env.ref('uom.product_uom_unit').id,
'product_uom_qty': 1.0,
'state': 'draft',
'type': 'add',
'company_id': obj().env.ref('base.main_company').id,
})]"/>
<field name="fees_lines" model="repair.fee" eval="[(5, 0, 0), (0, 0, {
'name': obj().env.ref('repair.product_service_order_repair').get_product_multiline_description_sale(),
'product_id': obj().env.ref('repair.product_service_order_repair').id,
'product_uom_qty': 1.0,
'product_uom': obj().env.ref('uom.product_uom_unit').id,
'price_unit': 50.0,
'repair_line_type': 'add',
'company_id': obj().env.ref('base.main_company').id,
})]"/>
<field name="partner_id" ref="base.res_partner_12"/>
+3 -2
View File
@@ -2,9 +2,10 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import repair
from . import stock_move
from . import stock_picking
from . import stock_traceability
from . import stock_lot
from . import account_move
from . import product
from . import mail_compose_message
from . import sale_order
from . import stock_warehouse
-22
View File
@@ -1,22 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
class AccountMove(models.Model):
_inherit = 'account.move'
repair_ids = fields.One2many('repair.order', 'invoice_id', readonly=True, copy=False)
def unlink(self):
repairs = self.sudo().repair_ids.filtered(lambda repair: repair.state != 'cancel')
if repairs:
repairs.sudo(False).state = '2binvoiced'
return super().unlink()
class AccountMoveLine(models.Model):
_inherit = 'account.move.line'
repair_line_ids = fields.One2many('repair.line', 'invoice_line_id', readonly=True, copy=False)
repair_fee_ids = fields.One2many('repair.fee', 'invoice_line_id', readonly=True, copy=False)
+11 -5
View File
@@ -1,18 +1,24 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
from odoo import fields, models
class Product(models.Model):
_inherit = "product.product"
def _count_returned_sn_products(self, sn_lot):
res = self.env['repair.line'].search_count([
('type', '=', 'remove'),
res = self.env['stock.move'].search_count([
('repair_line_type', '=', 'remove'),
('product_uom_qty', '=', 1),
('lot_id', '=', sn_lot.id),
('move_line_ids.lot_id', 'in', sn_lot.id),
('state', '=', 'done'),
('location_dest_id.usage', '=', 'internal'),
('location_dest_usage', '=', 'internal'),
])
return super()._count_returned_sn_products(sn_lot) + res
class ProductTemplate(models.Model):
_inherit = "product.template"
create_repair = fields.Boolean('Create Repair', help="Create a linked Repair Order on Sale Order confirmation of this product.", groups='stock.group_stock_user')
File diff suppressed because it is too large Load Diff
+112
View File
@@ -0,0 +1,112 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.tools import float_compare
class SaleOrder(models.Model):
_inherit = 'sale.order'
repair_order_ids = fields.One2many(
comodel_name='repair.order', inverse_name='sale_order_id',
string='Repair Order', groups='stock.group_stock_user')
repair_count = fields.Integer(
"Repair Order(s)", compute='_compute_repair_count', groups='stock.group_stock_user')
@api.depends('repair_order_ids')
def _compute_repair_count(self):
for order in self:
order.repair_count = len(order.repair_order_ids)
def _action_cancel(self):
res = super()._action_cancel()
self.order_line._cancel_repair_order()
return res
def _action_confirm(self):
res = super()._action_confirm()
self.order_line._create_repair_order()
return res
def action_show_repair(self):
self.ensure_one()
if self.repair_count == 1:
return {
"type": "ir.actions.act_window",
"res_model": "repair.order",
"views": [[False, "form"]],
"res_id": self.repair_order_ids.id,
}
elif self.repair_count > 1:
return {
"name": _("Repair Orders"),
"type": "ir.actions.act_window",
"res_model": "repair.order",
"view_mode": "tree,form",
"domain": [('sale_order_id', '=', self.id)],
}
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
def _compute_qty_delivered(self):
remaining_so_lines = self
for so_line in self:
move = so_line.move_ids.sudo().filtered(lambda m: m.repair_id and m.state == 'done')
if len(move) != 1:
continue
remaining_so_lines -= so_line
so_line.qty_delivered = move.quantity_done
return super(SaleOrderLine, remaining_so_lines)._compute_qty_delivered()
@api.model_create_multi
def create(self, vals_list):
res = super().create(vals_list)
res.filtered(lambda line: line.state in ('sale', 'done'))._create_repair_order()
return res
def write(self, vals_list):
old_product_uom_qty = {line.id: line.product_uom_qty for line in self}
res = super().write(vals_list)
for line in self:
if line.state in ('sale', 'done'):
if float_compare(old_product_uom_qty[line.id], 0, precision_rounding=line.product_uom.rounding) <= 0 and float_compare(line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) > 0:
self._create_repair_order()
if float_compare(old_product_uom_qty[line.id], 0, precision_rounding=line.product_uom.rounding) > 0 and float_compare(line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) <= 0:
self._cancel_repair_order()
return res
def _action_launch_stock_rule(self, previous_product_uom_qty=False):
# Picking must be generated for products created from the SO but not for parts added from the RO, as they're already handled there
lines_without_repair_move = self.filtered(lambda line: not line.move_ids.sudo().repair_id)
return super(SaleOrderLine, lines_without_repair_move)._action_launch_stock_rule(previous_product_uom_qty)
def _create_repair_order(self):
new_repair_vals = []
for line in self:
# One RO for each line with at least a quantity of 1, quantities > 1 don't create multiple ROs
if any(line.id == ro.sale_order_line_id.id for ro in line.order_id.sudo().repair_order_ids) and float_compare(line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) > 0:
binded_ro_ids = line.order_id.sudo().repair_order_ids.filtered(lambda ro: ro.sale_order_line_id.id == line.id and ro.state == 'cancel')
binded_ro_ids.action_repair_cancel_draft()
binded_ro_ids._action_repair_confirm()
continue
if not line.product_template_id.sudo().create_repair or line.move_ids.sudo().repair_id or float_compare(line.product_uom_qty, 0, precision_rounding=line.product_uom.rounding) <= 0:
continue
order = line.order_id
new_repair_vals.append({
'state': 'confirmed',
'partner_id': order.partner_id.id,
'sale_order_id': order.id,
'sale_order_line_id': line.id,
'picking_type_id': order.warehouse_id.repair_type_id.id,
})
if new_repair_vals:
self.env['repair.order'].sudo().create(new_repair_vals)
def _cancel_repair_order(self):
# Each RO binded to a SO line with Qty set to 0 or cancelled is set to 'Cancelled'
binded_ro_ids = self.env['repair.order']
for line in self:
binded_ro_ids |= line.order_id.sudo().repair_order_ids.filtered(lambda ro: ro.sale_order_line_id.id == line.id and ro.state != 'done')
binded_ro_ids.action_repair_cancel()
+4 -2
View File
@@ -3,6 +3,7 @@
from collections import defaultdict
from odoo import api, fields, models, _
class StockLot(models.Model):
_inherit = 'stock.lot'
@@ -13,8 +14,9 @@ class StockLot(models.Model):
@api.depends('name')
def _compute_repair_order_ids(self):
repair_orders = defaultdict(lambda: self.env['repair.order'])
for repair_line in self.env['repair.line'].search([('lot_id', 'in', self.ids), ('state', '=', 'done')]):
repair_orders[repair_line.lot_id.id] |= repair_line.repair_id
for repair_line in self.env['stock.move'].search([('repair_id', '!=', False), ('move_line_ids.lot_id', 'in', self.ids), ('state', '=', 'done')]):
for rl_id in repair_line.lot_ids.ids:
repair_orders[rl_id] |= repair_line.repair_id
for lot in self:
lot.repair_order_ids = repair_orders[lot.id]
lot.repair_order_count = len(lot.repair_order_ids)
+190
View File
@@ -0,0 +1,190 @@
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, Command, fields, models
from odoo.tools.misc import groupby
MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR = {
'add': {'location_id': 'location_id', 'location_dest_id': 'location_dest_id'},
'remove': {'location_id': 'location_dest_id', 'location_dest_id': 'parts_location_id'},
'recycle': {'location_id': 'location_dest_id', 'location_dest_id': 'recycle_location_id'},
}
class StockMove(models.Model):
_inherit = 'stock.move'
repair_id = fields.Many2one('repair.order', check_company=True)
repair_line_type = fields.Selection([
('add', 'Add'),
('remove', 'Remove'),
('recycle', 'Recycle')
], 'Type', store=True, index=True)
@api.depends('repair_line_type')
def _compute_forecast_information(self):
moves_to_compute = self.filtered(lambda move: not move.repair_line_type or move.repair_line_type == 'add')
for move in (self - moves_to_compute):
move.forecast_availability = move.product_qty
move.forecast_expected_date = False
return super(StockMove, moves_to_compute)._compute_forecast_information()
@api.depends('repair_id.picking_type_id')
def _compute_picking_type_id(self):
remaining_moves = self
for move in self:
if move.repair_id:
move.picking_type_id = move.repair_id.picking_type_id
remaining_moves -= move
return super(StockMove, remaining_moves)._compute_picking_type_id()
def copy_data(self, default=None):
default = dict(default or {})
if 'repair_id' in default or self.repair_id:
default['sale_line_id'] = False
return super().copy_data(default)
@api.ondelete(at_uninstall=False)
def _unlink_if_draft_or_cancel(self):
self.filtered('repair_id')._action_cancel()
return super()._unlink_if_draft_or_cancel()
def unlink(self):
self._clean_repair_sale_order_line()
return super().unlink()
@api.model_create_multi
def create(self, vals_list):
for vals in vals_list:
if not vals.get('repair_id') or 'repair_line_type' not in vals:
continue
repair_id = self.env['repair.order'].browse([vals['repair_id']])
vals['name'] = repair_id.name
src_location, dest_location = self._get_repair_locations(vals['repair_line_type'], repair_id)
if not vals.get('location_id'):
vals['location_id'] = src_location.id
if not vals.get('location_dest_id'):
vals['location_dest_id'] = dest_location.id
moves = super().create(vals_list)
repair_moves = self.env['stock.move']
for move in moves:
if not move.repair_id:
continue
move.group_id = move.repair_id.procurement_group_id.id
move.origin = move.name
move.picking_type_id = move.repair_id.picking_type_id.id
repair_moves |= move
if move.state == 'draft' and move.repair_id.state in ('confirmed', 'under_repair'):
move._check_company()
move._adjust_procure_method()
move._action_confirm()
move._trigger_scheduler()
repair_moves._create_repair_sale_order_line()
return moves
def write(self, vals):
res = super().write(vals)
repair_moves = self.env['stock.move']
moves_to_create_so_line = self.env['stock.move']
for move in self:
if not move.repair_id:
continue
# checks vals update
if 'repair_line_type' in vals or 'picking_type_id' in vals and move.product_id != move.repair_id.product_id:
move.location_id, move.location_dest_id = move._get_repair_locations(move.repair_line_type)
if not move.sale_line_id and 'sale_line_id' not in vals and move.repair_line_type == 'add':
moves_to_create_so_line |= move
if move.sale_line_id and ('repair_line_type' in vals or 'product_uom_qty' in vals):
repair_moves |= move
repair_moves._update_repair_sale_order_line()
moves_to_create_so_line._create_repair_sale_order_line()
return res
# Needed to also cancel the lastly added part
def _action_cancel(self):
self._clean_repair_sale_order_line()
return super()._action_cancel()
def _create_repair_sale_order_line(self):
if not self:
return
so_line_vals = []
for move in self:
if move.sale_line_id or move.repair_line_type != 'add' or not move.repair_id.sale_order_id:
continue
product_qty = move.product_uom_qty if move.repair_id.state != 'done' else move.quantity_done
so_line_vals.append({
'order_id': move.repair_id.sale_order_id.id,
'product_id': move.product_id.id,
'product_uom_qty': product_qty, # When relying only on so_line compute method, the sol quantity is only updated on next sol creation
'move_ids': [Command.link(move.id)],
})
if move.repair_id.under_warranty:
so_line_vals[-1]['price_unit'] = 0.0
elif move.price_unit:
so_line_vals[-1]['price_unit'] = move.price_unit
self.env['sale.order.line'].create(so_line_vals)
def _clean_repair_sale_order_line(self):
self.filtered(
lambda m: m.repair_id and m.sale_line_id
).mapped('sale_line_id').write({'product_uom_qty': 0.0})
def _update_repair_sale_order_line(self):
if not self:
return
moves_to_clean = self.env['stock.move']
moves_to_update = self.env['stock.move']
for move in self:
if not move.repair_id:
continue
if move.sale_line_id and move.repair_line_type != 'add':
moves_to_clean |= move
if move.sale_line_id and move.repair_line_type == 'add':
moves_to_update |= move
moves_to_clean._clean_repair_sale_order_line()
for sale_line, _ in groupby(moves_to_update, lambda m: m.sale_line_id):
sale_line.product_uom_qty = sum(sale_line.move_ids.mapped('product_uom_qty'))
def _is_consuming(self):
return super()._is_consuming() or (self.repair_id and self.repair_line_type == 'add')
def _get_repair_locations(self, repair_line_type, repair_id=False):
location_map = MAP_REPAIR_LINE_TYPE_TO_MOVE_LOCATIONS_FROM_REPAIR.get(repair_line_type)
if location_map:
if not repair_id:
self.repair_id.ensure_one()
repair_id = self.repair_id
location_id, location_dest_id = [repair_id[field] for field in location_map.values()]
else:
location_id, location_dest_id = False, False
return location_id, location_dest_id
def _get_source_document(self):
return self.repair_id or super()._get_source_document()
def _set_repair_locations(self):
moves_per_repair = self.filtered(lambda m: (m.repair_id and m.repair_line_type) is not False).grouped('repair_id')
if not moves_per_repair:
return
for moves in moves_per_repair.values():
grouped_moves = moves.grouped('repair_line_type')
for line_type, m in grouped_moves.items():
m.location_id, m.location_dest_id = m._get_repair_locations(line_type)
def _should_be_assigned(self):
res = super(StockMove, self)._should_be_assigned()
return bool(res and not (self.repair_id and self.repair_line_type))
def _create_extra_move(self):
if self.repair_id:
return self
return super(StockMove, self)._create_extra_move()
def _split(self, qty, restrict_partner_id=False):
# When setting the Repair Order as done with partially done moves, do not split these moves
if self.repair_id:
return []
return super(StockMove, self)._split(qty, restrict_partner_id)
+66
View File
@@ -1,24 +1,90 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import time
from odoo import _, api, fields, models
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT
class PickingType(models.Model):
_inherit = 'stock.picking.type'
code = fields.Selection(selection_add=[
('repair_operation', 'Repair')
], ondelete={'repair_operation': 'cascade'})
count_repair_ready = fields.Integer(
string="Number of Repair Orders to Process", compute='_compute_count_repair')
count_repair_waiting = fields.Integer(
string="Number of Repair Orders Waiting", compute='_compute_count_repair')
count_repair_late = fields.Integer(
string="Number of Repair Orders Late", compute='_compute_count_repair')
default_location_dest_id = fields.Many2one(
readonly={"code", "=", "repair_operation"})
default_remove_location_dest_id = fields.Many2one(
'stock.location', 'Default Remove Destination Location',
check_company=True, readonly=True,
help="This is the default remove destination location when you create a repair order with this operation type.")
default_recycle_location_dest_id = fields.Many2one(
'stock.location', 'Default Recycle Destination Location',
check_company=True,
help="This is the default recycle destination location when you create a repair order with this operation type.")
is_repairable = fields.Boolean(
'Create Repair Orders from Returns',
compute='_compute_is_repairable', store=True, readonly=False,
help="If ticked, you will be able to directly create repair orders from a return.")
return_type_of_ids = fields.One2many('stock.picking.type', 'return_picking_type_id')
def _compute_count_repair(self):
repair_picking_types = self.filtered(lambda picking: picking.code == 'repair_operation')
# By default, set count_repair_xxx to False
self.count_repair_ready = False
self.count_repair_waiting = False
self.count_repair_late = False
# shortcut
if not repair_picking_types:
return
domains = {
'count_repair_ready': [
('is_parts_available', '=', True)
],
'count_repair_waiting': [],
'count_repair_late': [
'|',
('schedule_date', '<', time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)),
('is_parts_late', '=', True)
],
}
for field, domain in domains.items():
picking_types = self.env['repair.order'].read_group(
[('picking_type_id', 'in', repair_picking_types.ids), ('state', 'in', ('confirmed', 'under_repair'))] + domain,
fields=['picking_type_id'],
groupby=['picking_type_id']
)
counts = {pt['picking_type_id'][0]:pt['picking_type_id_count'] for pt in picking_types}
for record in repair_picking_types:
record[field] = counts.get(record.id)
@api.depends('return_type_of_ids', 'code')
def _compute_is_repairable(self):
for picking_type in self:
if not(picking_type.code == 'incoming' and picking_type.return_type_of_ids):
picking_type.is_repairable = False
def get_repair_stock_picking_action_picking_type(self):
action = self.env["ir.actions.actions"]._for_xml_id('repair.action_picking_repair')
if self:
action['display_name'] = self.display_name
return action
class Picking(models.Model):
_inherit = 'stock.picking'
+51
View File
@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
class StockWarehouse(models.Model):
_inherit = 'stock.warehouse'
repair_type_id = fields.Many2one('stock.picking.type', 'Repair Operation Type', check_company=True)
def _get_sequence_values(self, name=False, code=False):
values = super(StockWarehouse, self)._get_sequence_values(name=name, code=code)
values.update({
'repair_type_id': {
'name': self.name + ' ' + _('Sequence repair'),
'prefix': self.code + '/RO/',
'padding': 5,
'company_id': self.company_id.id
},
})
return values
def _get_picking_type_create_values(self, max_sequence):
data, next_sequence = super(StockWarehouse, self)._get_picking_type_create_values(max_sequence)
prod_location = self.env['stock.location'].search([('usage', '=', 'production'), ('company_id', '=', self.company_id.id)], limit=1)
scrap_location = self.env['stock.location'].search([('scrap_location', '=', True), ('company_id', 'in', [self.company_id.id, False])], limit=1)
data.update({
'repair_type_id': {
'name': _('Repairs'),
'code': 'repair_operation',
'default_location_src_id': self.lot_stock_id.id,
'default_location_dest_id': prod_location.id,
'default_remove_location_dest_id':scrap_location.id,
'default_recycle_location_dest_id': self.lot_stock_id.id,
'sequence': next_sequence + 1,
'sequence_code': 'RO',
'company_id': self.company_id.id,
},
})
return data, max_sequence + 2
def _get_picking_type_update_values(self):
data = super(StockWarehouse, self)._get_picking_type_update_values()
data.update({
'repair_type_id': {
'active': self.active,
'barcode': self.code.replace(" ", "").upper() + "-RO",
},
})
return data
+4
View File
@@ -0,0 +1,4 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import stock_forecasted
+2 -4
View File
@@ -2,14 +2,12 @@
<odoo>
<data>
<record id="action_report_repair_order" model="ir.actions.report">
<field name="name">Quotation / Order</field>
<field name="name">Repair Order</field>
<field name="model">repair.order</field>
<field name="report_type">qweb-pdf</field>
<field name="report_name">repair.report_repairorder2</field>
<field name="report_file">repair.report_repairorder</field>
<field name="print_report_name">(
object.state == 'draft' and 'Repair Quotation - %s' % (object.name) or
'Repair Order - %s' % (object.name))</field>
<field name="print_report_name">('Repair Order - %s' % (object.name))</field>
<field name="binding_model_id" ref="model_repair_order"/>
<field name="binding_type">report</field>
</record>
@@ -5,148 +5,65 @@
<t t-set="o" t-value="doc"/>
<t t-call="web.external_layout">
<t t-set="o" t-value="o.with_context(lang=o.partner_id.lang)" />
<t t-set="information_block">
<strong t-if="o.address_id == o.partner_invoice_id">Invoice and shipping address:</strong>
<div t-if="o.partner_invoice_id">
<strong t-if="o.address_id != o.partner_invoice_id">Invoice address: </strong>
<div t-field="o.partner_invoice_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone", "vat"], "no_marker": True, "phone_icons": True}'/>
</div>
<t t-if="o.address_id != o.partner_invoice_id">
<strong>Shipping address:</strong>
<div t-field="o.address_id"
t-options='{"widget": "contact", "fields": ["address", "name", "phone", "vat"], "no_marker": True, "phone_icons": True}'/>
</t>
</t>
<t t-set="address">
<div t-field="o.partner_id"
t-options='{"widget": "contact", "fields": ["address", "name"], "no_marker": True}' />
</t>
<div class="page">
<div class="oe_structure"/>
<h2>
<span t-if="o.state != 'draft'">Repair Order #:</span>
<span t-if="o.state == 'draft'">Repair Quotation #:</span>
<span>Repair Order #:</span>
<span t-field="o.name"/>
</h2>
<div id="informations" class="row mt32 mb32">
<div t-if="o.product_id.name" class="col-3 bm-2">
<strong>Product to Repair:</strong>
<p t-field="o.product_id.name" class="m-0"/>
<div id="informations" class="row mb-3">
<div class="col-6">
<p t-if="o.partner_id" class="m-0">
<strong>Customer:</strong>
<span t-field="o.partner_id"/>
</p>
<p t-if="o.product_id" class="m-0">
<strong>Product:</strong>
<span t-field="o.product_id"/>
</p>
<p t-if="o.lot_id" class="m-0" groups="stock.group_production_lot">
<strong>Lot/Serial:</strong>
<span t-field="o.lot_id"/>
</p>
</div>
<div class="col-3 bm-2" groups="stock.group_production_lot">
<strong>Lot/Serial Number:</strong>
<t t-if="o.lot_id">
<span t-field="o.lot_id.name"/>
</t>
</div>
<div t-if="o.guarantee_limit" class="col-3 bm-2">
<strong>Warranty:</strong>
<p t-field="o.guarantee_limit" class="m-0"/>
</div>
<div class="col-3 bm-2">
<strong>Printing Date:</strong>
<p t-esc="datetime.datetime.now().strftime('%Y-%m-%d')" t-options="{'widget': 'date'}" class="m-0"/>
<div class="col-6">
<p class="m-0">
<strong>Status:</strong>
<span t-field="o.state"/>
</p>
<p t-if="o.user_id" class="m-0">
<strong>Responsible:</strong>
<span t-field="o.user_id"/>
</p>
</div>
</div>
<h2 class="mb-3 border-bottom border-2 border-dark">Parts</h2>
<table class="table table-sm o_main_table">
<thead>
<tr>
<th>Description</th>
<th class="text-end">Quantity</th>
<t t-if="o.invoice_method != 'none'">
<th class="text-end">Unit Price</th>
<th class="text-center">Tax</th>
<th class="text-end">Price</th>
</t>
</tr>
</thead>
<tbody>
<t t-if="o.operations">
<tr class="bg-200 o_line_section"><td colspan="5"><strong>Parts</strong></td></tr>
<tr t-foreach="o.operations" t-as="line">
<t t-if="o.move_ids">
<tr t-foreach="o.move_ids" t-as="line">
<td>
<p t-if="line.type == 'add'"><i>(Add)</i> <span t-field="line.name" /></p>
<p t-if="line.type == 'remove'">(<i>Remove</i>) <span t-field="line.name"/></p>
<p t-if="line.repair_line_type == 'add'"><i>(Add)</i> <span t-field="line.product_id" /></p>
<p t-if="line.repair_line_type == 'remove'">(<i>Remove</i>) <span t-field="line.product_id"/></p>
</td>
<td class="text-end">
<span t-field="line.product_uom_qty"/>
<span groups="uom.group_uom" t-field="line.product_uom.name"/>
</td>
<t t-if="(line.repair_id.invoice_method != 'none')">
<td class="text-end">
<span t-field="line.price_unit"/>
</td>
<td class="text-center">
<span t-esc="','.join(map( lambda x: x.description or x.name, line.tax_id))"/>
</td>
<td class="text-end o_price_total">
<span t-field="line.price_subtotal" t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</t>
</tr>
</t>
<t t-if="o.fees_lines">
<tr class="bg-200 o_line_section"><td colspan="5"><strong>Operations</strong></td></tr>
<tr t-foreach="o.fees_lines" t-as="fees">
<td>
<span t-field="fees.name"/>
</td>
<td class="text-end">
<span t-field="fees.product_uom_qty"/>
<span groups="uom.group_uom" t-field="fees.product_uom.name"/>
</td>
<t t-if="(fees.repair_id.invoice_method != 'none')">
<td class="text-end">
<span t-field="fees.price_unit"/>
</td>
<td class="text-center">
<span t-esc="','.join(map( lambda x: x.description or x.name, fees.tax_id))"/>
</td>
<td class="text-end o_price_total">
<span t-field="fees.price_subtotal"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</t>
</tr>
</t>
</tbody>
</table>
<div id="total" class="row justify-content-end">
<div class="col-4">
<table class="table table-sm">
<t t-if="o.invoice_method !='none'">
<tr class="border-black o_subtotal">
<td><strong>Total Without Taxes</strong></td>
<td class="text-end">
<span t-field="o.amount_untaxed"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
<tr>
<td>Taxes</td>
<td class="text-end o_price_total">
<span t-field="o.amount_tax"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
<tr class="border-black o_total">
<td><strong>Total</strong></td>
<td class="text-end o_price_total">
<span t-field="o.amount_total"
t-options='{"widget": "monetary", "display_currency": o.currency_id}'/>
</td>
</tr>
</t>
</table>
</div>
<div t-if="o.internal_notes">
<h2 class="mb-3 border-bottom border-2 border-dark">Repair Notes</h2>
<p t-field="o.internal_notes"/>
</div>
<p t-field="o.quotation_notes"/>
<div class="oe_structure"/>
</div>
</t>
+30
View File
@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class StockForecasted(models.AbstractModel):
_inherit = 'stock.forecasted_product_product'
def _get_reservation_data(self, move):
if move.repair_id and move.repair_line_type:
return False
return super()._get_reservation_data(move)
def _product_sale_domain(self, product_template_ids, product_ids):
"""
When a product's move is bind at the same time to a Repair Order
and to a Sale Order, only take the data into account once, as a RO
"""
sol_domain = super(StockForecasted, self)._product_sale_domain(product_template_ids, product_ids)
move_domain = self._product_domain(product_template_ids, product_ids)
move_domain += [
('repair_id', '!=', False),
('sale_line_id', '!=', False),
('repair_line_type', '=', 'add')
]
sol_ids = self.env['stock.move']._read_group(move_domain, aggregates=['sale_line_id:array_agg'])[0][0]
sol_domain += [('id', 'not in', sol_ids)]
return sol_domain
+1 -4
View File
@@ -1,8 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_repair_fee_user,Repair Fee user,model_repair_fee,stock.group_stock_user,1,1,1,1
access_repair_user,Repair user,model_repair_order,stock.group_stock_user,1,1,1,1
access_repair_tag_user,Repair Tags user,model_repair_tags,stock.group_stock_user,1,1,1,1
access_repair_line_user,repair.line user,model_repair_line,stock.group_stock_user,1,1,1,1
access_account_tax_user,account.tax,account.model_account_tax,stock.group_stock_user,1,0,0,0
access_repair_order_make_invoice,access.repair.order.make_invoice,model_repair_order_make_invoice,stock.group_stock_user,1,1,1,0
access_stock_warn_insufficient_qty_repair,access.stock.warn.insufficient.qty.repair,model_stock_warn_insufficient_qty_repair,stock.group_stock_user,1,1,1,0
access_repair_warn_uncomplete_move,access.repair.warn.uncomplete.move,model_repair_warn_uncomplete_move,stock.group_stock_user,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
access_repair_fee_user Repair Fee user model_repair_fee stock.group_stock_user 1 1 1 1
2 access_repair_user Repair user model_repair_order stock.group_stock_user 1 1 1 1
3 access_repair_tag_user Repair Tags user model_repair_tags stock.group_stock_user 1 1 1 1
access_repair_line_user repair.line user model_repair_line stock.group_stock_user 1 1 1 1
access_account_tax_user account.tax account.model_account_tax stock.group_stock_user 1 0 0 0
access_repair_order_make_invoice access.repair.order.make_invoice model_repair_order_make_invoice stock.group_stock_user 1 1 1 0
4 access_stock_warn_insufficient_qty_repair access.stock.warn.insufficient.qty.repair model_stock_warn_insufficient_qty_repair stock.group_stock_user 1 1 1 0
5 access_repair_warn_uncomplete_move access.repair.warn.uncomplete.move model_repair_warn_uncomplete_move stock.group_stock_user 1 1 1 0
@@ -7,15 +7,5 @@
<field name="model_id" search="[('model','=','repair.order')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="repair_line_rule">
<field name="name">repair line multi-company</field>
<field name="model_id" search="[('model','=','repair.line')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
<record model="ir.rule" id="repair_fee_rule">
<field name="name">repair fee multi-company</field>
<field name="model_id" search="[('model','=','repair.fee')]" model="ir.model"/>
<field name="domain_force">[('company_id', 'in', company_ids)]</field>
</record>
</odoo>
+328 -269
View File
@@ -1,15 +1,16 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.account.tests.common import AccountTestInvoicingCommon
from odoo.tests import tagged, Form
from odoo.exceptions import UserError, ValidationError
from odoo.tests import tagged, common, Form
from odoo.tools import float_compare, float_is_zero
@tagged('post_install', '-at_install')
class TestRepair(AccountTestInvoicingCommon):
class TestRepair(common.TransactionCase):
@classmethod
def setUpClass(cls, chart_template_ref=None):
super().setUpClass(chart_template_ref=chart_template_ref)
def setUpClass(cls):
super().setUpClass()
# Partners
cls.res_partner_1 = cls.env['res.partner'].create({'name': 'Wood Corner'})
@@ -18,15 +19,42 @@ class TestRepair(AccountTestInvoicingCommon):
# Products
cls.product_product_3 = cls.env['product.product'].create({'name': 'Desk Combination'})
cls.product_product_11 = cls.env['product.product'].create({'name': 'Conference Chair'})
cls.product_product_11 = cls.env['product.product'].create({
'name': 'Conference Chair',
'lst_price': 30.0,
})
cls.product_product_5 = cls.env['product.product'].create({'name': 'Product 5'})
cls.product_product_6 = cls.env['product.product'].create({'name': 'Large Cabinet'})
cls.product_product_12 = cls.env['product.product'].create({'name': 'Office Chair Black'})
cls.product_product_13 = cls.env['product.product'].create({'name': 'Corner Desk Left Sit'})
cls.product_product_2 = cls.env['product.product'].create({'name': 'Virtual Home Staging'})
cls.product_service_order_repair = cls.env['product.product'].create({
'name': 'Repair Services',
'type': 'service',
# Storable products
cls.product_storable_no = cls.env['product.product'].create({
'name': 'Product Storable No Tracking',
'type': 'product',
'tracking': 'none',
})
cls.product_storable_serial = cls.env['product.product'].create({
'name': 'Product Storable Serial',
'type': 'product',
'tracking': 'serial',
})
cls.product_storable_lot = cls.env['product.product'].create({
'name': 'Product Storable Lot',
'type': 'product',
'tracking': 'lot',
})
# 'Create Repair' Products
cls.product_consu_order_repair = cls.env['product.product'].create({
'name': 'Repair Consumable',
'type': 'consu',
'create_repair': True,
})
cls.product_storable_order_repair = cls.env['product.product'].create({
'name': 'Repair Storable',
'type': 'product',
'create_repair': True,
})
# Location
@@ -38,35 +66,15 @@ class TestRepair(AccountTestInvoicingCommon):
# Repair Orders
cls.repair1 = cls.env['repair.order'].create({
'address_id': cls.res_partner_address_1.id,
'guarantee_limit': '2019-01-01',
'invoice_method': 'none',
'user_id': False,
'product_id': cls.product_product_3.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'partner_invoice_id': cls.res_partner_address_1.id,
'location_id': cls.stock_warehouse.lot_stock_id.id,
'operations': [
'picking_type_id': cls.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'location_dest_id': cls.product_product_11.property_stock_production.id,
'location_id': cls.stock_warehouse.lot_stock_id.id,
'name': cls.product_product_11.get_product_multiline_description_sale(),
'product_id': cls.product_product_11.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'product_uom_qty': 1.0,
'price_unit': 50.0,
'state': 'draft',
'type': 'add',
'company_id': cls.env.company.id,
})
],
'fees_lines': [
(0, 0, {
'name': cls.product_service_order_repair.get_product_multiline_description_sale(),
'product_id': cls.product_service_order_repair.id,
'product_uom_qty': 1.0,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'price_unit': 50.0,
'repair_line_type': 'add',
'company_id': cls.env.company.id,
})
],
@@ -76,33 +84,14 @@ class TestRepair(AccountTestInvoicingCommon):
cls.repair0 = cls.env['repair.order'].create({
'product_id': cls.product_product_5.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'address_id': cls.res_partner_address_1.id,
'guarantee_limit': '2019-01-01',
'invoice_method': 'after_repair',
'user_id': False,
'partner_invoice_id': cls.res_partner_address_1.id,
'location_id': cls.stock_warehouse.lot_stock_id.id,
'operations': [
'picking_type_id': cls.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'location_dest_id': cls.product_product_12.property_stock_production.id,
'location_id': cls.stock_warehouse.lot_stock_id.id,
'name': cls.product_product_12.get_product_multiline_description_sale(),
'price_unit': 50.0,
'product_id': cls.product_product_12.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'product_uom_qty': 1.0,
'state': 'draft',
'type': 'add',
'company_id': cls.env.company.id,
})
],
'fees_lines': [
(0, 0, {
'name': cls.product_service_order_repair.get_product_multiline_description_sale(),
'product_id': cls.product_service_order_repair.id,
'product_uom_qty': 1.0,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'price_unit': 50.0,
'repair_line_type': 'add',
'company_id': cls.env.company.id,
})
],
@@ -112,33 +101,14 @@ class TestRepair(AccountTestInvoicingCommon):
cls.repair2 = cls.env['repair.order'].create({
'product_id': cls.product_product_6.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'address_id': cls.res_partner_address_1.id,
'guarantee_limit': '2019-01-01',
'invoice_method': 'b4repair',
'user_id': False,
'partner_invoice_id': cls.res_partner_address_1.id,
'location_id': cls.stock_location_14.id,
'operations': [
'picking_type_id': cls.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'location_dest_id': cls.product_product_13.property_stock_production.id,
'location_id': cls.stock_warehouse.lot_stock_id.id,
'name': cls.product_product_13.get_product_multiline_description_sale(),
'price_unit': 50.0,
'product_id': cls.product_product_13.id,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'product_uom_qty': 1.0,
'state': 'draft',
'type': 'add',
'company_id': cls.env.company.id,
})
],
'fees_lines': [
(0, 0, {
'name': cls.product_service_order_repair.get_product_multiline_description_sale(),
'product_id': cls.product_service_order_repair.id,
'product_uom_qty': 1.0,
'product_uom': cls.env.ref('uom.product_uom_unit').id,
'price_unit': 50.0,
'repair_line_type': 'add',
'company_id': cls.env.company.id,
})
],
@@ -147,188 +117,304 @@ class TestRepair(AccountTestInvoicingCommon):
cls.env.user.groups_id |= cls.env.ref('stock.group_stock_user')
def _create_simple_repair_order(self, invoice_method):
def _create_simple_repair_order(self):
product_to_repair = self.product_product_5
partner = self.res_partner_address_1
return self.env['repair.order'].create({
'product_id': product_to_repair.id,
'product_uom': product_to_repair.uom_id.id,
'address_id': partner.id,
'guarantee_limit': '2019-01-01',
'invoice_method': invoice_method,
'partner_invoice_id': partner.id,
'location_id': self.stock_warehouse.lot_stock_id.id,
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'partner_id': self.res_partner_12.id
})
def _create_simple_operation(self, repair_id=False, qty=0.0, price_unit=0.0):
product_to_add = self.product_product_5
return self.env['repair.line'].create({
'name': 'Add The product',
'type': 'add',
'product_id': product_to_add.id,
def _create_simple_part_move(self, repair_id=False, qty=0.0, product=False):
if not product:
product = self.product_product_5
return self.env['stock.move'].create({
'repair_line_type': 'add',
'product_id': product.id,
'product_uom_qty': qty,
'product_uom': product_to_add.uom_id.id,
'price_unit': price_unit,
'repair_id': repair_id,
'location_id': self.stock_warehouse.lot_stock_id.id,
'location_dest_id': product_to_add.property_stock_production.id,
'company_id': self.env.company.id,
})
def _create_simple_fee(self, repair_id=False, qty=0.0, price_unit=0.0):
product_service = self.product_product_2
return self.env['repair.fee'].create({
'name': 'PC Assemble + Custom (PC on Demand)',
'product_id': product_service.id,
'product_uom_qty': qty,
'product_uom': product_service.uom_id.id,
'price_unit': price_unit,
'repair_id': repair_id,
'company_id': self.env.company.id,
})
def test_00_repair_afterinv(self):
repair = self._create_simple_repair_order('after_repair')
self._create_simple_operation(repair_id=repair.id, qty=1.0, price_unit=50.0)
# I confirm Repair order taking Invoice Method 'After Repair'.
repair.action_repair_confirm()
@classmethod
def create_quant(cls, product, qty, offset=0, name="L"):
i = 1
if product.tracking == 'serial':
i, qty = qty, 1
if name == "L":
name = "S"
vals = []
for x in range(1, i + 1):
qDict = {
'location_id': cls.stock_warehouse.lot_stock_id.id,
'product_id': product.id,
'inventory_quantity': qty,
}
if product.tracking != 'none':
qDict['lot_id'] = cls.env['stock.lot'].create({
'name': name + str(offset + x),
'product_id': product.id,
'company_id': cls.env.company.id
}).id
vals.append(qDict)
return cls.env['stock.quant'].create(vals)
def test_01_repair_states_transition(self):
repair = self._create_simple_repair_order()
# Draft -> Confirmed -> Cancel -> Draft -> Done -> Failing Cancel
# draft -> confirmed (action_validate -> _action_repair_confirm)
# PRE
# lines' qty >= 0 !-> UserError
# product's qty IS available !-> Warning w/ choice
# POST
# state = confirmed
# move_ids in (partially reserved, fully reserved, waiting availability)
# Line A with qty < 0 --> UserError
lineA = self._create_simple_part_move(repair.id, -1.0, self.product_storable_no)
repair.move_ids |= lineA
with self.assertRaises(UserError):
repair.action_validate()
# Line A with qty > 0 & not available, Line B with qty >= 0 & available --> Warning (stock.warn.insufficient.qty.repair)
lineA.product_uom_qty = 2.0
lineB = self._create_simple_part_move(repair.id, 2.0, self.product_storable_lot)
repair.move_ids |= lineB
quant = self.create_quant(self.product_storable_no, 1)
quant |= self.create_quant(self.product_storable_lot, 3)
quant.action_apply_inventory()
lineC = self._create_simple_part_move(repair.id, 1.0, self.product_storable_order_repair)
repair.move_ids |= lineC
repair.product_id = self.product_storable_serial
validate_action = repair.action_validate()
self.assertEqual(validate_action.get("res_model"), "stock.warn.insufficient.qty.repair")
# Warn qty Wizard only apply to "product TO repair"
warn_qty_wizard = Form(
self.env['stock.warn.insufficient.qty.repair']
.with_context(**validate_action['context'])
).save()
warn_qty_wizard.action_done()
# I check the state is in "Confirmed".
self.assertEqual(repair.state, "confirmed", 'Repair order should be in "Confirmed" state.')
repair.action_repair_start()
self.assertEqual(lineA.state, "partially_available", 'Repair line #1 should be in "Partial Availability" state.')
self.assertEqual(lineB.state, "assigned", 'Repair line #2 should be in "Available" state.')
self.assertEqual(lineC.state, "confirmed", 'Repair line #3 should be in "Waiting Availability" state.')
# I check the state is in "Under Repair".
self.assertEqual(repair.state, "under_repair", 'Repair order should be in "Under_repair" state.')
# Create quotation
# No partner warning -> working case -> already linked warning
# Repairing process for product is in Done state and I end Repair process by clicking on "End Repair" button.
repair.action_repair_end()
# Ensure SO doesn't exist
self.assertEqual(repair.sale_order_id.__len__(), 0)
repair.partner_id = None
with self.assertRaises(UserError) as err:
repair.action_create_sale_order()
self.assertIn("You need to define a customer", err.exception.args[0])
repair.partner_id = self.res_partner_12.id
repair.action_create_sale_order()
# Ensure SO and SOL were created
self.assertNotEqual(repair.sale_order_id.__len__(), 0)
self.assertEqual(repair.sale_order_id.order_line.__len__(), 3)
with self.assertRaises(UserError) as err:
repair.action_create_sale_order()
self.assertIn("You cannot create a quotation for a repair order that is already linked", err.exception.args[0])
# I define Invoice Method 'After Repair' option in this Repair order.so I create invoice by clicking on "Make Invoice" wizard.
make_invoice = self.env['repair.order.make_invoice'].create({
'group': True})
# I click on "Create Invoice" button of this wizard to make invoice.
context = {
"active_model": 'repair_order',
"active_ids": [repair.id],
"active_id": repair.id
}
make_invoice.with_context(context).make_invoices()
# (*) -> cancel (action_repair_cancel)
# PRE
# state != done !-> UserError (cf. end of this test)
# POST
# moves_ids state == cancelled
# 'Lines" SOL product_uom_qty == 0
# state == cancel
# I check that invoice is created for this Repair order.
self.assertEqual(len(repair.invoice_id), 1, "No invoice exists for this repair order")
self.assertEqual(len(repair.move_id.move_line_ids[0].consume_line_ids), 1, "Consume lines should be set")
def test_01_repair_b4inv(self):
repair = self._create_simple_repair_order('b4repair')
# I confirm Repair order for Invoice Method 'Before Repair'.
repair.action_repair_confirm()
# I click on "Create Invoice" button of this wizard to make invoice.
repair.action_repair_invoice_create()
# I check that invoice is created for this Repair order.
self.assertEqual(len(repair.invoice_id), 1, "No invoice exists for this repair order")
def test_02_repair_noneinv(self):
repair = self._create_simple_repair_order('none')
# Add a new fee line
self._create_simple_fee(repair_id=repair.id, qty=1.0, price_unit=12.0)
self.assertEqual(repair.amount_total, 12, "Amount_total should be 12")
# Add new operation line
self._create_simple_operation(repair_id=repair.id, qty=1.0, price_unit=14.0)
self.assertEqual(repair.amount_total, 26, "Amount_total should be 26")
# I confirm Repair order for Invoice Method 'No Invoice'.
repair.action_repair_confirm()
# I start the repairing process by clicking on "Start Repair" button for Invoice Method 'No Invoice'.
repair.action_repair_start()
# I check its state which is in "Under Repair".
self.assertEqual(repair.state, "under_repair", 'Repair order should be in "Under_repair" state.')
# Repairing process for product is in Done state and I end this process by clicking on "End Repair" button.
repair.action_repair_end()
self.assertEqual(repair.move_id.location_id.id, self.stock_warehouse.lot_stock_id.id,
'Repaired product was taken in the wrong location')
self.assertEqual(repair.move_id.location_dest_id.id, self.stock_warehouse.lot_stock_id.id,
'Repaired product went to the wrong location')
self.assertEqual(repair.operations.move_id.location_id.id, self.stock_warehouse.lot_stock_id.id,
'Consumed product was taken in the wrong location')
self.assertEqual(repair.operations.move_id.location_dest_id.id, self.product_product_5.property_stock_production.id,
'Consumed product went to the wrong location')
# I define Invoice Method 'No Invoice' option in this repair order.
# So, I check that Invoice has not been created for this repair order.
self.assertNotEqual(len(repair.invoice_id), 1, "Invoice should not exist for this repair order")
def test_repair_state(self):
repair = self._create_simple_repair_order('b4repair')
repair.action_repair_confirm()
repair.action_repair_invoice_create()
repair.invoice_id.unlink()
# Repair order state should be changed to 2binvoiced so that new invoice can be created
self.assertEqual(repair.state, '2binvoiced', 'Repair order should be in 2binvoiced state, if invoice is deleted.')
repair.action_repair_invoice_create()
self.assertNotEqual(repair.state, "done")
repair.action_repair_cancel()
# Repair order and linked invoice both should be cancelled.
self.assertEqual(repair.state, 'cancel', 'Repair order should be in cancel state.')
self.assertEqual(repair.invoice_id.state, 'cancel', 'Invoice should be in cancel state.')
self.assertEqual(repair.state, "cancel")
self.assertTrue(all(m.state == "cancel" for m in repair.move_ids))
self.assertTrue(all(float_is_zero(sol.product_uom_qty, 2) for sol in repair.sale_order_id.order_line))
# (*)/cancel -> draft (action_repair_cancel_draft)
# PRE
# state == cancel !-> action_repair_cancel()
# state != done !~> UserError (transitive..., don't care)
# POST
# move_ids.state == draft
# state == draft
repair.action_repair_cancel_draft()
# Linked invoice should be unlinked
self.assertEqual(len(repair.invoice_id), 0, "No invoice should be exists for this repair order")
self.assertEqual(repair.state, "draft")
self.assertTrue(all(m.state == "draft" for m in repair.move_ids))
def test_03_repair_multicompany(self):
""" This test ensures that the correct taxes are selected when the user fills in the RO form """
# draft -> confirmed
# Enforce product_id availability to skip warning
quant = self.create_quant(self.product_storable_serial, 1)
quant.action_apply_inventory()
repair.lot_id = quant.lot_id
repair.action_validate()
self.assertEqual(repair.state, "confirmed")
company01 = self.env.company
company02 = self.env['res.company'].create({
'name': 'SuperCompany',
'country_id': self.env.ref('base.us').id,
})
# confirmed -> under_repair (action_repair_start)
# Purely informative state
repair.action_repair_start()
self.assertEqual(repair.state, "under_repair")
self.env["account.tax.group"].create([{
"name": "default",
"company_id": company01.id
}, {
"name": "default",
"company_id": company02.id
}])
tax01 = self.env["account.tax"].create({
"name": "C01 Tax",
"amount": "0.00",
"company_id": company01.id
})
tax02 = self.env["account.tax"].create({
"name": "C02 Tax",
"amount": "0.00",
"company_id": company02.id
})
# under_repair -> done (action_repair_end -> action_repair_done)
# PRE
# state == under_repair !-> UserError
# lines' qty_done >= lines' product_uom_qty !-> Warning
# line tracked => line has lot_ids !-> ValidationError
# POST
# lines with qty_done == 0 are cancelled (related sol product_uom_qty is consequently set to 0)
# repair.product_id => repair.move_id
# move_ids.state == (done || cancel)
# state == done
# move_ids with qty_done (LOWER or HIGHER than) product_uom_qty MUST NOT be splitted
# Any line with qty_done < product_uom_qty => Warning
end_action = repair.action_repair_end()
self.assertEqual(end_action.get("res_model"), "repair.warn.uncomplete.move")
warn_uncomplete_wizard = Form(
self.env['repair.warn.uncomplete.move']
.with_context(**end_action['context'])
).save()
# LineB : no serial => ValidationError
with self.assertRaises(ValidationError) as err:
warn_uncomplete_wizard.action_validate()
self.assertIn("Serial number is required for operation lines", err.exception.args[0])
super_product = self.env['product.template'].create({
"name": "SuperProduct",
"taxes_id": [(4, tax01.id), (4, tax02.id)],
})
super_variant = super_product.product_variant_id
self.assertEqual(super_variant.taxes_id, tax01 | tax02)
# LineB with lots
move_line = lineB.move_line_ids[0]
move_line.qty_done = move_line.reserved_uom_qty
ro_form = Form(self.env['repair.order'])
ro_form.product_id = super_variant
ro_form.partner_id = company01.partner_id
with ro_form.operations.new() as ro_line:
ro_line.product_id = super_variant
with ro_form.fees_lines.new() as fee_line:
fee_line.product_id = super_variant
repair_order = ro_form.save()
lineA.quantity_done = 2 # qty_done = product_uom_qty
lineC.quantity_done = 2 # qty_done > product_uom_qty (No warning)
lineD = self._create_simple_part_move(repair.id, 0.0)
repair.move_ids |= lineD # product_uom_qty = 0 : state is cancelled
# tax02 should not be present since it belongs to the second company.
self.assertEqual(repair_order.operations.tax_id, tax01)
self.assertEqual(repair_order.fees_lines.tax_id, tax01)
self.assertEqual(lineD.state, 'assigned')
num_of_lines = repair.move_ids.__len__()
self.assertEqual(repair.move_id.__len__(), 0)
repair.action_repair_end()
self.assertEqual(repair.state, "done")
done_moves = repair.move_ids - lineD
#line a,b,c are 'done', line d is 'cancel'
self.assertTrue(all(m.state == 'done' for m in done_moves))
self.assertEqual(lineD.state, 'cancel')
self.assertEqual(repair.move_id.__len__(), 1)
self.assertEqual(repair.move_ids.__len__(), num_of_lines) # No split
# (*) -> cancel (action_repair_cancel)
# PRE
# state != done !-> UserError
with self.assertRaises(UserError) as err:
repair.action_repair_cancel()
self.assertEqual(err.exception.args[0], "You cannot cancel a Repair Order that's already been completed")
def test_02_repair_sale_order_binding(self):
# Binding from SO to RO(s)
# On SO Confirm
# - Create linked RO per line (only if item with "create_repair" checked)
# Create Repair SOL
# - sol qty updated to 0 -> RO canceled (Reciprocal is true too)
# - sol qty back to >0 -> RO Confirmed (Reciprocal is not true)
# RO Parts SOL
# - SOL qty change is NOT propagated to RO
# - However, these changes FROM RO are propagated to SO
#----------------------------------------------------------------------------------
# Binding from RO to SO
so_form = Form(self.env['sale.order'])
so_form.partner_id = self.res_partner_1
with so_form.order_line.new() as line:
line.product_id = self.product_consu_order_repair
# line.product_template_id = self.product_consu_order_repair.product_tmpl_id
line.product_uom_qty = 2.0
sale_order = so_form.save()
order_line = sale_order.order_line[0]
self.assertEqual(len(sale_order.repair_order_ids), 0)
sale_order.action_confirm()
# Quantity set on the "create repair" product doesn't affect the number of RO created
self.assertEqual(len(sale_order.repair_order_ids), 1)
repair_order = sale_order.repair_order_ids[0]
self.assertEqual(sale_order, repair_order.sale_order_id)
self.assertEqual(repair_order.state, 'confirmed')
order_line.product_uom_qty = 0
self.assertEqual(repair_order.state, 'cancel')
order_line.product_uom_qty = 1
self.assertEqual(repair_order.state, 'confirmed')
repair_order.action_repair_cancel()
self.assertTrue(float_is_zero(order_line.product_uom_qty, 2))
order_line.product_uom_qty = 3
self.assertEqual(repair_order.state, 'confirmed')
# Add RO line
ro_form = Form(repair_order)
with ro_form.move_ids.new() as ro_line_form:
ro_line_form.repair_line_type = 'add'
ro_line_form.product_id = self.product_product_11
ro_line_form.product_uom_qty = 1
ro_form.save()
ro_line_0 = repair_order.move_ids[0]
sol_part_0 = ro_line_0.sale_line_id
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# chg qty in SO -> No effect on RO
sol_part_0.product_uom_qty = 5
self.assertNotEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# chg qty in RO -> Update qty in SO
ro_line_0.product_uom_qty = 3
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# with/without warranty
self.assertFalse(float_is_zero(sol_part_0.price_unit, 2))
repair_order.under_warranty = True
self.assertTrue(float_is_zero(sol_part_0.price_unit, 2))
repair_order.under_warranty = False
self.assertFalse(float_is_zero(sol_part_0.price_unit, 2))
# stock_move transitions
# add -> remove -> add -> recycle -> add transitions
ro_line_0.repair_line_type = 'remove'
self.assertTrue(float_is_zero(sol_part_0.product_uom_qty, 2))
ro_line_0.repair_line_type = 'add'
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
ro_line_0.repair_line_type = 'recycle'
self.assertTrue(float_is_zero(sol_part_0.product_uom_qty, 2))
ro_line_0.repair_line_type = 'add'
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.product_uom_qty, 2), 0)
# remove and recycle line : not added to SO.
sol_count = len(sale_order.order_line)
with ro_form.move_ids.new() as ro_line_form:
ro_line_form.repair_line_type = 'remove'
ro_line_form.product_id = self.product_product_12
ro_line_form.product_uom_qty = 1
with ro_form.move_ids.new() as ro_line_form:
ro_line_form.repair_line_type = 'recycle'
ro_line_form.product_id = self.product_product_13
ro_line_form.product_uom_qty = 1
ro_form.save()
ro_line_1 = repair_order.move_ids[1]
self.assertEqual(len(sale_order.order_line), sol_count)
# remove to add -> added to SO
ro_line_1.repair_line_type = 'add'
sol_part_1 = ro_line_1.sale_line_id
self.assertNotEqual(len(sale_order.order_line), sol_count)
self.assertEqual(float_compare(sol_part_1.product_uom_qty, ro_line_1.product_uom_qty, 2), 0)
# delete 'remove to add' line in RO -> SOL qty set to 0
repair_order.move_ids = [(2, ro_line_1.id, 0)]
self.assertTrue(float_is_zero(sol_part_1.product_uom_qty, 2))
# repair_order.action_repair_end()
# -> order_line.qty_delivered == order_line.product_uom_qty
# -> "RO Lines"'s SOL.qty_delivered == move.quantity_done
for line in repair_order.move_ids:
line.quantity_done = line.product_uom_qty
repair_order.action_repair_start()
repair_order.action_repair_end()
self.assertEqual(float_compare(order_line.product_uom_qty, order_line.qty_delivered, 2), 0)
self.assertEqual(float_compare(sol_part_0.product_uom_qty, ro_line_0.quantity_done, 2), 0)
self.assertTrue(float_is_zero(sol_part_1.qty_delivered, 2))
def test_repair_return(self):
"""Tests functionality of creating a repair directly from a return picking,
@@ -357,60 +443,33 @@ class TestRepair(AccountTestInvoicingCommon):
def test_repair_compute_product_uom(self):
repair = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'operations': [
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'name': 'foo',
'repair_line_type': 'add',
'product_id': self.product_product_11.id,
'price_unit': 50.0,
})
],
})
self.assertEqual(repair.product_uom, self.product_product_3.uom_id)
self.assertEqual(repair.operations[0].product_uom, self.product_product_11.uom_id)
self.assertEqual(repair.move_ids[0].product_uom, self.product_product_11.uom_id)
def test_repair_compute_location(self):
repair = self.env['repair.order'].create({
'product_id': self.product_product_3.id,
'operations': [
'picking_type_id': self.stock_warehouse.repair_type_id.id,
'move_ids': [
(0, 0, {
'name': 'foo',
'repair_line_type': 'add',
'product_id': self.product_product_11.id,
'price_unit': 50.0,
})
],
})
self.assertEqual(repair.location_id, self.stock_warehouse.lot_stock_id)
self.assertEqual(repair.operations[0].location_id, self.stock_warehouse.lot_stock_id)
self.assertEqual(repair.move_ids[0].location_id, self.stock_warehouse.lot_stock_id)
location_dest_id = self.env['stock.location'].search([
('usage', '=', 'production'),
('company_id', '=', repair.company_id.id),
], limit=1)
self.assertEqual(repair.operations[0].location_dest_id, location_dest_id)
self.assertEqual(repair.move_ids[0].location_dest_id, location_dest_id)
def test_repair_order_send_to_self(self):
# when sender(logged in user) is also present in recipients of the mail composer,
# user should receive mail.
product_to_repair = self.product_product_5
partner = self.res_partner_address_1
repair_order = self.env['repair.order'].with_user(self.env.user).create({
'product_id': product_to_repair.id,
'product_uom': product_to_repair.uom_id.id,
'address_id': partner.id,
'guarantee_limit': '2019-01-01',
'location_id': self.stock_warehouse.lot_stock_id.id,
'partner_id': self.env.user.partner_id.id
})
email_ctx = repair_order.action_send_mail().get('context', {})
# We need to prevent auto mail deletion, and so we copy the template and send the mail with
# added configuration in copied template. It will allow us to check whether mail is being
# sent to to author or not (in case author is present in 'Recipients' of composer).
mail_template = self.env['mail.template'].browse(email_ctx.get('default_template_id')).copy({'auto_delete': False})
# send the mail with same user as customer
repair_order.with_context(**email_ctx).with_user(self.env.user).message_post_with_source(
mail_template,
subtype_xmlid='mail.mt_comment',
)
mail_message = repair_order.message_ids[0]
self.assertEqual(mail_message.author_id, repair_order.partner_id, 'Repair: author should be same as customer')
self.assertEqual(mail_message.author_id, mail_message.partner_ids, 'Repair: author should be in composer recipients thanks to "partner_to" field set on template')
self.assertEqual(mail_message.partner_ids, mail_message.sudo().mail_ids.recipient_ids, 'Repair: author should receive mail due to presence in composer recipients')
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_product_template_form_inherit_repair" model="ir.ui.view">
<field name="name">product.template.form.inherit.repair</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_form_view"/>
<field name="arch" type="xml">
<xpath expr="//group[@name='group_general']/field[@name='uom_po_id']" position="after">
<field name="create_repair" attrs="{'invisible':[('detailed_type', 'not in', ('consu', 'product', 'service'))]}"/>
</xpath>
</field>
</record>
</odoo>
+118 -163
View File
@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_repair_order_tree" model="ir.ui.view">
@@ -11,14 +12,18 @@
<field name="priority" optional="show" widget="priority" nolabel="1"/>
<field name="name"/>
<field name="schedule_date" optional="show" widget="remaining_days"/>
<field name="description" optional="hide"/>
<field name="product_id" readonly="1" optional="show"/>
<field name="parts_availability_state" invisible="1" options='{"lazy": true}'/>
<field name="parts_availability" options='{"lazy": true}'
attrs="{'invisible': [('state', 'not in', ['confirmed', 'under_repair'])]}"
optional="show"
decoration-success="parts_availability_state == 'available'"
decoration-warning="parts_availability_state == 'expected'"
decoration-danger="parts_availability_state == 'late'"/>
<field name="product_qty" optional="hide" string="Quantity"/>
<field name="product_uom" string="Unit of Measure" readonly="1" groups="uom.group_uom" optional="hide"/>
<field name="user_id" optional="hide" widget='many2one_avatar_user'/>
<field name="partner_id" readonly="1" optional="show"/>
<field name="address_id" optional="show"/>
<field name="guarantee_limit" optional="show"/>
<field name="picking_id" optional="hide"/>
<field name="is_returned" optional="hide"/>
<field name="sale_order_id" optional="show"/>
@@ -35,38 +40,35 @@
<field name="model">repair.order</field>
<field name="arch" type="xml">
<form string="Repair Order">
<field name="unreserve_visible" invisible="1"/>
<field name="reserve_visible" invisible="1"/>
<field name="show_set_qty_button" invisible="1"/>
<field name="show_clear_qty_button" invisible="1"/>
<header>
<button name="action_validate" states="draft" type="object" string="Confirm Repair" class="oe_highlight" data-hotkey="v"/>
<button name="action_repair_start" attrs="{'invisible': ['&amp;', ('state','!=','confirmed'), '!', '&amp;', ('state','=','ready'), ('invoice_method','=','b4repair')]}"
type="object" string="Start Repair" class="oe_highlight" data-hotkey="q"/>
<button name="action_repair_end" states="under_repair" type="object" string="End Repair" class="oe_highlight" data-hotkey="w"/>
<button name="action_repair_invoice_create" type="object" string="Create Invoice" class="oe_highlight" groups="account.group_account_invoice" attrs="{'invisible': ['|', ('state', '!=', '2binvoiced'), ('invoice_id', '!=', False)]}" data-hotkey="w"/>
<button name="action_send_mail" states="draft" string="Send Quotation" type="object" data-hotkey="g"/>
<button name="print_repair_order" states="draft" string="Print Quotation" type="object" data-hotkey="y"/>
<button name="action_repair_cancel_draft" states="cancel" string="Set to Draft" type="object" data-hotkey="x"/>
<button name="action_repair_cancel" string="Cancel Repair" type="object" confirm="Draft invoices for this order will be cancelled. Do you confirm the action?" attrs="{'invisible':['|', ('state', 'in', ('cancel', 'done')), ('invoice_state', '!=', 'draft')]}" data-hotkey="l"/>
<button name="action_repair_cancel" string="Cancel Repair" type="object" attrs="{'invisible': ['|', ('state','in', ('cancel','done')), ('invoice_state', '=', 'draft')]}" data-hotkey="l"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,done"/>
<button name="action_repair_start" states="confirmed" type="object" string="Start Repair" class="oe_highlight" data-hotkey="q"/>
<button name="action_repair_end" states="under_repair" type="object" string="End Repair" class="oe_highlight" data-hotkey="x"/>
<button name="action_set_quantities_to_reservation" attrs="{'invisible': [('show_set_qty_button', '=', False)]}" string="Set quantities" type="object" groups="stock.group_stock_user" class="o_btn_validate" data-hotkey="g"/>
<button name="action_clear_quantities_to_zero" attrs="{'invisible': [('show_clear_qty_button', '=', False)]}" string="Clear quantities" type="object" groups="stock.group_stock_user" class="o_btn_validate" data-hotkey="g"/>
<button name="action_assign" attrs="{'invisible': ['|', ('state', 'in', ('draft', 'done', 'cancel')), ('reserve_visible', '=', False)]}" string="Check availability" type="object"/>
<button name="action_unreserve" type="object" string="Unreserve" attrs="{'invisible': [('unreserve_visible', '=', False)]}" data-hotkey="w"/>
<button name="action_create_sale_order" type="object" string="Create Quotation" attrs="{'invisible': ['|', ('state','=', 'cancel'), ('sale_order_id', '!=', False)]}"/>
<button name="action_repair_cancel" string="Cancel Repair" type="object" attrs="{'invisible': [('state','in', ('done', 'cancel'))]}" data-hotkey="l"/>
<button name="action_repair_cancel_draft" states="cancel" string="Set to Draft" type="object" data-hotkey="z"/>
<field name="state" widget="statusbar" statusbar_visible="draft,confirmed,under_repair,done"/>
</header>
<sheet string="Repairs order">
<div class="oe_button_box" name="button_box">
<field name="invoice_id" invisible="1"/>
<button name="%(action_repair_move_lines)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ['done', 'cancel'])]}">
<div class="o_stat_info">
<span class="o_stat_text">Product Moves</span>
</div>
</button>
<button name="action_created_invoice"
type="object"
class="oe_stat_button"
icon="fa-pencil-square-o"
attrs="{'invisible': [('invoice_id', '=', False)]}"
groups="account.group_account_invoice">
<!-- No groups attribute on the next button as "stock.group_stock_user" is needed for Repair, and as this group is granted 'sale.order' read/write accesses in sale_stock module (forcefully loaded as transitive dependency) -->
<button name="action_view_sale_order" type="object" string="Sale Order" icon="fa-dollar" class="oe_stat_button" attrs="{'invisible': [('sale_order_id', '==', False)]}">
<div class="o_field_widget o_stat_info">
<span class="o_stat_text">1</span>
<span class="o_stat_text">Invoices</span>
<span class="o_stat_value">
<field name="sale_order_id" widget="statinfo" nolabel="1" class="mr4"/>
</span>
<span class="o_stat_text">Sale Order</span>
</div>
</button>
<button name="%(action_repair_move_lines)d" type="action" string="Product Moves" class="oe_stat_button" icon="fa-exchange" attrs="{'invisible': [('state', 'not in', ['done', 'cancel'])]}"/>
</div>
<div class="oe_title">
<label class="o_form_label" for="name"/>
@@ -77,157 +79,96 @@
</div>
<group>
<group>
<field name="description"/>
<field name="invoice_state" invisible="1"/>
<field name="tracking" invisible="1" attrs="{'readonly': 1}"/>
<field name="company_id" invisible="1"/>
<field name="sale_order_id" invisible="1"/>
<field name="sale_order_line_id" invisible="1"/>
<field name="repair_request" attrs="{'invisible': [('sale_order_line_id', '=', False)]}"/>
<field name="partner_id" widget="res_partner_many2one" context="{'res_partner_search_mode': 'customer', 'show_vat': True}" attrs="{'readonly':[('sale_order_id', '!=', False)]}"/>
<field name="product_id"/>
<field name="lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" groups="stock.group_production_lot" attrs="{'required':[('tracking', 'in', ['serial', 'lot'])], 'invisible': [('tracking', 'not in', ['serial', 'lot'])], 'readonly': ['|', ('state', '=', 'done'), ('tracking', 'not in', ['serial', 'lot'])]}"/>
<field name="lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" groups="stock.group_production_lot" options="{'no_create': True, 'no_create_edit': True}" attrs="{'required':[('tracking', 'in', ['serial', 'lot'])], 'invisible': [('tracking', 'not in', ['serial', 'lot'])], 'readonly': ['|', ('state', '=', 'done'), ('tracking', 'not in', ['serial', 'lot'])]}"/>
<field name="product_uom_category_id" invisible="1"/>
<label for="product_qty"/>
<div class="o_row">
<field name="product_qty" attrs="{'readonly':[('tracking', '=', 'serial')]}"/>
<label for="product_qty" attrs="{'invisible': [('product_id', '=', False)]}"/>
<div class="o_row" attrs="{'invisible': [('product_id', '=', False)]}">
<field name="product_qty" attrs="{'readonly':['|', ('tracking', '=', 'serial'), ('state', 'in', ('done','cancel'))]}"/>
<field name="product_uom" groups="uom.group_uom"/>
</div>
<field name="partner_id" widget="res_partner_many2one" attrs="{'required':[('invoice_method','!=','none')]}" context="{'res_partner_search_mode': 'customer', 'show_vat': True}"/>
<field name="address_id" groups="account.group_delivery_invoice_address"/>
<field name="sale_order_id"/>
<field name="allowed_picking_type_ids" invisible="1"/>
<field name="picking_id" domain="[('picking_type_id','in', allowed_picking_type_ids), ('product_id','=',product_id)]" options="{'no_create': True}"/>
<field name="picking_id" domain="[('picking_type_id','in', allowed_picking_type_ids)]" options="{'no_create': True}"/>
</group>
<group>
<field name="schedule_date"/>
<field name="schedule_date" attrs="{'readonly': [('state', 'in', ['done', 'cancel'])]}"/>
<field name="user_id" domain="[('share', '=', False)]"/>
<field name="location_id" options="{'no_create': True}"/>
<field name="company_id" groups="base.group_multi_company" options="{'no_create': True}"/>
<field name="guarantee_limit"/>
<field name="invoice_method"/>
<field name="partner_invoice_id" attrs="{'invisible':[('invoice_method','=', 'none')],'required':[('invoice_method','!=','none')]}" groups="account.group_delivery_invoice_address"/>
<field name="pricelist_id" groups="product.group_product_pricelist" context="{'product_id':product_id}" attrs="{'invisible':[('invoice_method','=', 'none')]}"/>
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color', 'no_create_edit': True}"/>
<field name="currency_id" invisible="1"/>
<field name="tax_calculation_rounding_method" invisible="1"/>
<field name="under_warranty"/>
</group>
</group>
<notebook>
<page string="Parts" name="parts">
<field name="operations" context="{'default_product_uom_qty': product_qty, 'default_company_id': company_id}" attrs="{'readonly':[('state', 'in', ['done', 'cancel'])]}">
<form string="Operations">
<group>
<group>
<field name="company_id" invisible="1" force_save="1"/>
<field name="type"/>
<field name="product_id"/>
<field name="name"/>
<field name="product_uom_category_id" invisible="1"/>
<label for="product_uom_qty"/>
<div class="o_row">
<field name="product_uom_qty"/>
<field name="product_uom" groups="uom.group_uom"/>
</div>
<field name="price_unit"/>
<field name="tax_id" widget="many2many_tags"/>
<field name="invoiced" invisible="1"/>
<field name="price_subtotal" widget="monetary" invisible="1"/>
<field name="currency_id" invisible="1"/>
</group>
<group>
<field name="lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" groups="stock.group_production_lot"/>
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
<field name="location_dest_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations"/>
</group>
</group>
<group name="History" string="History" attrs="{'invisible':[('move_id','=', False)]}">
<field name="move_id"/>
<field name="invoice_line_id" invisible="1"/>
</group>
</form>
<field name="move_ids" attrs="{'readonly': ['|', ('state', '=', 'cancel'), ('state', '=', 'done')]}"
context="{'default_repair_id': id, 'default_product_uom_qty': product_qty, 'default_company_id': company_id, 'default_date': schedule_date, 'default_repair_line_type': 'add'}">
<tree string="Operations" editable="bottom">
<field name="company_id" invisible="1" force_save="1"/>
<field name="type"/>
<field name="product_id"/>
<field name='name' optional="show"/>
<field name="company_id" invisible="1"/>
<field name="name" invisible="1"/>
<field name="state" invisible="1"/>
<field name="repair_line_type" required="1"/>
<field name="picking_type_id" invisible="1"/>
<field name="location_id" invisible="1"/>
<field name="location_dest_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="scrapped" invisible="1"/>
<field name="picking_code" invisible="1"/>
<field name="product_type" invisible="1"/>
<field name="show_details_visible" invisible="1"/>
<field name="show_reserved_availability" invisible="1"/>
<field name="show_operations" invisible="1" readonly="1"/>
<field name="additional" invisible="1"/>
<field name="move_lines_count" invisible="1"/>
<field name="is_locked" invisible="1"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="tracking" invisible="1"/>
<field name="lot_id" context="{'default_product_id': product_id, 'default_company_id': company_id}" groups="stock.group_production_lot" attrs="{'readonly':[('tracking', 'not in', ['serial', 'lot'])]}"/>
<field name="location_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations" optional="show"/>
<field name="location_dest_id" options="{'no_create': True}" groups="stock.group_stock_multi_locations" optional="show"/>
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" string="UoM" groups="uom.group_uom" optional="show"/>
<field name="price_unit"/>
<field name="tax_id" widget="many2many_tags" optional="show"/>
<field name="price_subtotal" string="Tax excl." widget="monetary"/>
<field name="price_total" string="Tax incl." widget="monetary"
attrs="{'column_invisible': [('parent.tax_calculation_rounding_method', '=', 'round_globally')]}"
optional="hide"/>
<field name="currency_id" invisible="1"/>
<field name="has_tracking" invisible="1"/>
<field name="display_assign_serial" invisible="1"/>
<field name="product_id" required="1" context="{'default_detailed_type': 'product'}" attrs="{'readonly': ['|', '&amp;', ('state', '!=', 'draft'), ('additional', '=', False), ('move_lines_count', '&gt;', 0)]}"/>
<field name="description_picking" string="Description" optional="hide"/>
<field name="date" optional="hide"/>
<field name="date_deadline" optional="hide"/>
<field name="is_initial_demand_editable" invisible="1"/>
<field name="is_quantity_done_editable" invisible="1"/>
<field name="product_packaging_id" groups="product.group_stock_packaging"/>
<field name="product_uom_qty" string="Demand" attrs="{'readonly': [('state', 'in', ('done', 'cancel'))]}"/>
<field name="forecast_expected_date" invisible="1"/>
<field name="reserved_availability" invisible="1"/>
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': ['&amp;', ('forecast_availability', '&lt;', 0), ('repair_line_type', '=', 'add')]}"/>
<button type="object" name="action_product_forecast_report" title="Forecast Report" icon="fa-area-chart text-danger" attrs="{'column_invisible': [('parent.state', '!=', 'draft')], 'invisible': ['|', ('forecast_availability', '&gt;=', 0), ('repair_line_type', '!=', 'add')]}"/>
<field name="forecast_availability" string="Reserved" attrs="{'column_invisible': [('parent.state', 'in', ('draft', 'done'))]}" widget="forecast_widget"/>
<field name="product_qty" invisible="1" readonly="1"/>
<field name="quantity_done" string="Done" attrs="{'readonly': [('product_id', '=', False)]}"/>
<field name="product_uom" attrs="{'readonly': [('state', '!=', 'draft'), ('additional', '=', False)]}" options="{'no_open': True, 'no_create': True}" string="Unit of Measure" groups="uom.group_uom"/>
<field name="lot_ids" widget="many2many_tags"
groups="stock.group_production_lot"
attrs="{'invisible': ['|', ('show_details_visible', '=', False), ('has_tracking', '!=', 'serial')]}"
optional="hide"
context="{'default_company_id': company_id, 'default_product_id': product_id}"
domain="[('product_id','=',product_id)]"
/>
<button name="action_show_details" type="object" icon="fa-list" width="0.1" title="Details"
attrs="{'invisible': [('show_details_visible', '=', False)]}" options='{"warn": true}'/>
</tree>
</field>
<group class="oe_subtotal_footer oe_right">
<field name="amount_untaxed" sum="Untaxed amount"/>
<field name="amount_tax"/>
<div class="oe_subtotal_footer_separator oe_inline o_td_label">
<label for="amount_total" />
<button name="button_dummy"
states="draft" string="(update)" type="object" class="oe_edit_only oe_link"/>
</div>
<field name="amount_total" nolabel="1" sum="Total amount" class="oe_subtotal_footer_separator"/>
</group>
<div class="clearfix"/>
</page>
<page string="Operations" name="operations">
<field name="fees_lines" context="{'default_company_id': company_id}" attrs="{'readonly':[('state', 'in', ['done', 'cancel'])]}">
<form string="Fees">
<group>
<field name="company_id" invisible="1" force_save="1"/>
<field name="product_id" required="True"/>
<field name="name"/>
<field name="product_uom_category_id" invisible="1"/>
<label for="product_uom_qty"/>
<div class="o_row">
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom" groups="uom.group_uom"/>
</div>
<field name="price_unit"/>
<field widget="many2many_tags" name="tax_id"/>
<field name="price_subtotal" widget="monetary" invisible="1"/>
<field name="currency_id" invisible="1"/>
</group>
</form>
<tree string="Fees" editable="bottom">
<field name="company_id" invisible="1" force_save="1"/>
<field name="product_id" required="True" context="{'default_type': 'service'}"/>
<field name='name' optional="show"/>
<field name="product_uom_qty" string="Quantity"/>
<field name="product_uom_category_id" invisible="1"/>
<field name="product_uom" string="Unit of Measure" groups="uom.group_uom" optional="show"/>
<field name="price_unit"/>
<field name="tax_id" widget="many2many_tags" optional="show"/>
<field name="price_subtotal" string="Tax excl." widget="monetary"/>
<field name="price_total" string="Tax incl." widget="monetary"
attrs="{'column_invisible': [('parent.tax_calculation_rounding_method', '=', 'round_globally')]}"
optional="hide"/>
<field name="currency_id" invisible="1"/>
<field name="currency_id" invisible="1"/>
</tree>
</field>
</page>
<page string="Extra Info" name="extra_info" groups="base.group_no_one">
<group>
<group>
<field name="move_id"/>
</group>
<group>
<field name="repaired"/>
<field name="invoiced"/>
</group>
</group>
</page>
<page string="Repair Notes">
<field name="internal_notes" placeholder="Add internal notes."/>
</page>
<page string="Quotation Notes">
<field name="quotation_notes" placeholder="Add quotation notes."/>
<page string="Miscellaneous">
<group>
<field name="picking_type_id" options="{'no_create': True}" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
</group>
<group string="Locations" groups="stock.group_stock_multi_locations" name="locations">
<field name="location_id" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_create': True}"/>
<field name="recycle_location_id" attrs="{'readonly': [('state', '!=', 'draft')]}" options="{'no_create': True}"/>
</group>
</page>
</notebook>
</sheet>
@@ -245,13 +186,11 @@
<field name="name">repair.kanban</field>
<field name="model">repair.order</field>
<field name="arch" type="xml">
<kanban class="o_kanban_mobile" sample="1">
<kanban class="o_kanban_mobile" sample="1" quick_create="false">
<field name="company_id" invisible="1"/>
<field name="name" />
<field name="product_id" />
<field name="name"/>
<field name="product_id"/>
<field name="partner_id"/>
<field name="address_id"/>
<field name="guarantee_limit"/>
<field name="state"/>
<field name="activity_state"/>
<progressbar field="activity_state" colors='{"planned": "success", "today": "warning", "overdue": "danger"}'/>
@@ -293,13 +232,12 @@
<field name="product_id"/>
<field name="partner_id" filter_domain="[('partner_id', 'child_of', self)]"/>
<field name="sale_order_id"/>
<filter string="Quotations" name="quotations" domain="[('state', '=', 'draft')]"/>
<filter string="Confirmed" domain="[('state', '=', 'confirmed')]" name="current" />
<filter string="Ready To Repair" name="ready_to_repair" domain="[('state', '=', 'ready')]"/>
<filter string="Ready To Repair" name="ready" domain="[('state', 'in', ('confirmed', 'under_repair')),('is_parts_available', '=', True)]"/>
<filter string="Waiting" name="filter_waiting" domain="[('state', 'in', ('confirmed', 'under_repair'))]"/>
<filter string="Late" name="filter_late" domain="[('state', 'in', ('confirmed', 'under_repair')), '|', ('schedule_date', '&lt;', context_today().strftime('%Y-%m-%d')), ('is_parts_late', '=', True)]"/>
<filter string="Returned" name="is_returned" domain="[('picking_id', '!=', False), ('picking_id.state', '=', 'done'), ('state', 'not in', ['cancel', 'done'])]"/>
<separator/>
<filter string="Invoiced" name="invoiced" domain="[('invoiced', '=', True)]"/>
<separator/>
<filter name="filter_create_date" date="create_date"/>
<separator/>
<filter invisible="1" string="Late Activities" name="activities_overdue"
@@ -313,7 +251,6 @@
<filter string="Customer" name="partner" domain="[]" context="{'group_by': 'partner_id'}"/>
<filter string="Product" name="product" domain="[]" context="{'group_by': 'product_id'}"/>
<filter string="Status" name="status" domain="[]" context="{'group_by': 'state'}"/>
<filter string="Warranty Expiration" name="guarantee_limit" domain="[]" context="{'group_by': 'guarantee_limit'}"/>
<filter string="Company" name="company" domain="[]" context="{'group_by': 'company_id'}" groups="base.group_multi_company"/>
</group>
</search>
@@ -342,6 +279,13 @@
</field>
</record>
<record id="action_repair_order_form" model="ir.actions.act_window">
<field name="name">Repair Orders</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">repair.order</field>
<field name="view_mode">form</field>
</record>
<record id="action_repair_order_tree" model="ir.actions.act_window">
<field name="name">Repair Orders</field>
<field name="res_model">repair.order</field>
@@ -370,6 +314,17 @@
<field name="view_id" ref="view_repair_graph"/>
</record>
<record id="action_picking_repair" model="ir.actions.act_window">
<field name="name">Repair Orders</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">repair.order</field>
<field name="view_mode">tree,kanban,form</field>
<field name="view_id" eval="False"/>
<field name="search_view_id" ref="view_repair_order_form_filter"/>
<field name="domain">[('picking_type_id', '=', active_id)]</field>
<field name="context">{'default_picking_type_id': active_id}</field>
</record>
<record id="view_repair_tag_form" model="ir.ui.view">
<field name="name">repair.tag.form</field>
<field name="model">repair.tags</field>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_sale_order_form_inherit_repair" model="ir.ui.view">
<field name="name">sale.order.form.inherit.repair</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//button[@name='action_view_invoice']" position="after">
<button
name="action_show_repair"
type="object"
class="oe_stat_button"
icon="fa-wrench"
attrs="{'invisible': [('repair_count', '=', 0)]}"
groups="stock.group_stock_user">
<field name="repair_count" widget="statinfo" string="Repairs"/>
</button>
</xpath>
</field>
</record>
</data>
</odoo>
+121
View File
@@ -5,12 +5,26 @@
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.view_picking_type_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='default_location_dest_id']" position="after">
<field name="default_remove_location_dest_id" options="{'no_create': True}" attrs="{'invisible': [('code', '!=', 'repair_operation')],'required': [('code', '=', 'repair_operation')]}"/>
<field name="default_recycle_location_dest_id" options="{'no_create': True}" attrs="{'invisible': [('code', '!=', 'repair_operation')],'required': [('code', '=', 'repair_operation')]}"/>
</xpath>
<xpath expr="//group[@name='locations']" position="after">
<field name="return_type_of_ids" invisible="1"/>
<group string="Repairs" attrs="{'invisible': ['|', ('code', '!=', 'incoming'), ('return_type_of_ids', '=', [])]}">
<field name="is_repairable"/>
</group>
</xpath>
<field name="create_backorder" position="attributes">
<attribute name="attrs">
{'invisible': [('code', '=', 'repair_operation')]}
</attribute>
</field>
<field name="show_operations" position="attributes">
<attribute name="attrs">
{'invisible': [('code', '=', 'repair_operation')]}
</attribute>
</field>
</field>
</record>
@@ -33,4 +47,111 @@
</xpath>
</field>
</record>
<record id="stock_repair_type_kanban" model="ir.ui.view">
<field name="name">stock.picking.type.kanban</field>
<field name="model">stock.picking.type</field>
<field name="inherit_id" ref="stock.stock_picking_type_kanban"/>
<field name="arch" type="xml">
<field name="code" position="after">
<field name="count_repair_ready"/>
<field name="count_repair_waiting"/>
<field name="count_repair_late"/>
</field>
<xpath expr="//t[@t-name='kanban-menu']" position="inside">
<div class="container" t-if="record.code.raw_value == 'repair_operation'">
<div class="row">
<div class="col-6 o_kanban_card_manage_section o_kanban_manage_view" name="picking_left_manage_pane">
<div role="menuitem" class="o_kanban_card_manage_title">
<span>Orders</span>
</div>
<div role="menuitem">
<a name="get_repair_stock_picking_action_picking_type" type="object">All</a>
</div>
<div role="menuitem">
<a name="get_repair_stock_picking_action_picking_type" context="{'search_default_ready': 1}" type="object">Parts Available</a>
</div>
<div role="menuitem">
<a name="get_repair_stock_picking_action_picking_type" context="{'search_default_filter_waiting': 1}" type="object">Waiting</a>
</div>
<div role="menuitem">
<a name="get_repair_stock_picking_action_picking_type" context="{'search_default_filter_late': 1}" type="object">Late</a>
</div>
</div>
<div class="col-6 o_kanban_card_manage_section o_kanban_manage_new">
<div role="menuitem" class="o_kanban_card_manage_title">
<span>New</span>
</div>
<div role="menuitem">
<a name="%(action_repair_order_form)d" type="action" context="{'default_picking_type_id': id}">Repair Order</a>
</div>
</div>
</div>
<div t-if="widget.editable" class="o_kanban_card_manage_settings row">
<div role="menuitem" aria-haspopup="true" class="col-8">
<ul role="menu" class="oe_kanban_colorpicker" data-field="color"/>
</div>
<div class="col-4">
<a class="dropdown-item" role="menuitem" type="edit">Configuration</a>
</div>
</div>
</div>
</xpath>
<xpath expr='//div[@name="stock_picking"]' position="after">
<div t-if="record.code.raw_value == 'repair_operation'" t-attf-class="#{!selection_mode ? kanban_color(record.color.raw_value) : ''}">
<div>
<div t-attf-class="o_kanban_card_header">
<div class="o_kanban_card_header_title">
<div class="o_primary" t-if="!selection_mode">
<a type="object" name="get_repair_stock_picking_action_picking_type">
<field name="name"/>
</a>
</div>
<span class="o_primary" t-if="selection_mode">
<field name="name"/>
</span>
<div class="o_secondary">
<field class="o_secondary" name="warehouse_id" readonly="1" groups="stock.group_stock_multi_warehouses"/>
</div>
</div>
</div>
<div class="container o_kanban_card_content" t-if="!selection_mode">
<div class="row">
<div class="col-6 o_kanban_primary_left">
<button class="btn btn-primary" name="get_repair_stock_picking_action_picking_type" context="{'search_default_ready': 1}" type="object">
<span t-if="record.code.raw_value == 'repair_operation'">
<t t-esc="record.count_repair_ready.value"/> To Process
</span>
</button>
</div>
<div class="col-6 o_kanban_primary_right">
<div t-if="record.count_repair_waiting.raw_value > 0" class="row">
<div class="col-9">
<a class="oe_kanban_stock_picking_type_list" name="get_repair_stock_picking_action_picking_type" context="{'search_default_filter_waiting': 1}" type="object">
Waiting
</a>
</div>
<div class="col-3">
<field name="count_repair_waiting"/>
</div>
</div>
<div t-if="record.count_repair_late.raw_value > 0" class="row">
<div class="col-9">
<a class="oe_kanban_stock_picking_type_list" name="get_repair_stock_picking_action_picking_type" context="{'search_default_filter_late': 1}" type="object">
Late
</a>
</div>
<div class="col-3">
<field name="count_repair_late"/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_warehouse_inherit_repair" model="ir.ui.view">
<field name="name">Stock Warehouse Inherit Repair</field>
<field name="model">stock.warehouse</field>
<field name="inherit_id" ref="stock.view_warehouse"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='out_type_id']" position="after">
<field name="repair_type_id" readonly="True"/>
</xpath>
</field>
</record>
</odoo>
+1 -1
View File
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import repair_make_invoice
from . import stock_warn_insufficient_qty
from . import repair_warn_uncomplete_move
@@ -1,34 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models
class MakeInvoice(models.TransientModel):
_name = 'repair.order.make_invoice'
_description = 'Create Mass Invoice (repair)'
group = fields.Boolean('Group by partner invoice address')
def make_invoices(self):
if not self._context.get('active_ids'):
return {'type': 'ir.actions.act_window_close'}
new_invoice = {}
for wizard in self:
repairs = self.env['repair.order'].browse(self._context['active_ids'])
new_invoice = repairs._create_invoices(group=wizard.group)
# We have to udpate the state of the given repairs, otherwise they remain 'to be invoiced'.
# Note that this will trigger another call to the method '_create_invoices',
# but that second call will not do anything, since the repairs are already invoiced.
repairs.action_repair_invoice_create()
return {
'domain': [('id', 'in', list(new_invoice.values()))],
'name': 'Invoices',
'view_mode': 'tree,form',
'res_model': 'account.move',
'view_id': False,
'views': [(self.env.ref('account.view_move_tree').id, 'tree'), (self.env.ref('account.view_move_form').id, 'form')],
'context': "{'move_type':'out_invoice'}",
'type': 'ir.actions.act_window'
}
@@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Make Invoice -->
<record id="view_make_invoice" model="ir.ui.view">
<field name="name">Make Invoice</field>
<field name="model">repair.order.make_invoice</field>
<field name="arch" type="xml">
<form string="Create invoices">
<group string="Do you really want to create the invoice(s)?">
<field name="group"/>
</group>
<footer>
<button name="make_invoices" string="Create Invoice" type="object" class="btn-primary" data-hotkey="q"/>
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x" />
</footer>
</form>
</field>
</record>
<record id="act_repair_invoice" model="ir.actions.act_window">
<field name="name">Create invoices</field>
<field name="res_model">repair.order.make_invoice</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="binding_model_id" ref="model_repair_order"/>
<field name="binding_view_types">list</field>
</record>
</data>
</odoo>
@@ -0,0 +1,14 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models
class RepairkWarnUncompleteMove(models.TransientModel):
_name = 'repair.warn.uncomplete.move'
_description = 'Warn Uncomplete Move(s)'
repair_ids = fields.Many2many('repair.order', string='Repair Orders')
def action_validate(self):
self.ensure_one()
return self.repair_ids.action_repair_done()
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="view_repair_warn_uncomplete_move" model="ir.ui.view">
<field name="name">repair.warn.uncomplete.move.view.form</field>
<field name="model">repair.warn.uncomplete.move</field>
<field name="arch" type="xml">
<form string="Uncomplete Move(s)">
<p>
For some of the parts, there is a difference between the initial demand and the actual quantity that was used.<br/>
Are you sure you want to confirm ?
</p>
<footer>
<button name="action_validate" string="Validate" type="object" class="btn-primary" />
<button string="Discard" special="cancel" data-hotkey="z" class="btn-secondary"/>
</footer>
</form>
</field>
</record>
</odoo>
@@ -3,6 +3,7 @@
from odoo import fields, models
class StockWarnInsufficientQtyRepair(models.TransientModel):
_name = 'stock.warn.insufficient.qty.repair'
_inherit = 'stock.warn.insufficient.qty'
@@ -15,4 +16,4 @@ class StockWarnInsufficientQtyRepair(models.TransientModel):
def action_done(self):
self.ensure_one()
return self.repair_id.action_repair_confirm()
return self.repair_id._action_repair_confirm()
@@ -41,6 +41,7 @@ class TestAngloSaxonValuation(ValuationReconciliationTestCommon):
'tax_id': False, # no love taxes amls
})],
})
sale_order.flush_recordset()
sale_order.action_confirm()
return sale_order