[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:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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">[
|
||||
'|',
|
||||
'|',
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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')
|
||||
|
||||
+475
-777
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import stock_forecasted
|
||||
@@ -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>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
|
@@ -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
@@ -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')
|
||||
|
||||
@@ -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>
|
||||
@@ -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': ['&', ('state','!=','confirmed'), '!', '&', ('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': ['|', '&', ('state', '!=', 'draft'), ('additional', '=', False), ('move_lines_count', '>', 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': ['&', ('forecast_availability', '<', 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', '>=', 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', '<', 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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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,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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user