[MOV] rma_product_cores: from Hibou Suite Enterprise for 13.0

This commit is contained in:
Jared Kipe
2020-07-03 09:16:34 -07:00
parent 9968143931
commit 1a02f43f51
12 changed files with 648 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models
from . import wizard

View File

@@ -0,0 +1,29 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'RMA - Product Cores',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '13.0.1.0.0',
'license': 'OPL-1',
'category': 'Tools',
'summary': 'RMA Product Cores',
'description': """
RMA Product Cores - Return core products from customers.
""",
'website': 'https://hibou.io/',
'depends': [
'product_cores',
'rma_sale',
],
'data': [
'views/portal_templates.xml',
'views/rma_views.xml',
'wizard/rma_lines_views.xml',
],
'demo': [
'data/rma_demo.xml',
],
'installable': True,
'application': False,
'auto_install': True,
}

View File

@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="template_product_core_sale_return" model="rma.template">
<field name="name">Core Sale Return</field>
<field name="usage">product_core_sale</field>
<field name="valid_days" eval="10"/>
<field name="create_in_picking" eval="True"/>
<field name="in_type_id" ref="stock.picking_type_in"/>
<field name="in_location_id" ref="stock.stock_location_customers"/>
<field name="in_location_dest_id" ref="stock.stock_location_stock"/>
<field name="in_procure_method">make_to_stock</field>
<field name="in_to_refund" eval="True"/>
<field name="in_require_return" eval="False"/>
<field name="so_decrement_order_qty" eval="False"/>
</record>
<record id="template_product_core_purchase_return" model="rma.template">
<field name="name">Core Purchase Return</field>
<field name="usage">product_core_purchase</field>
<field name="valid_days" eval="10"/>
<field name="create_out_picking" eval="True"/>
<field name="out_type_id" ref="stock.picking_type_out"/>
<field name="out_location_id" ref="stock.stock_location_stock"/>
<field name="out_location_dest_id" ref="stock.stock_location_suppliers"/>
<field name="out_procure_method">make_to_stock</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import rma

View File

@@ -0,0 +1,235 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class RMATemplate(models.Model):
_inherit = 'rma.template'
usage = fields.Selection(selection_add=[
('product_core_sale', 'Product Core Sale'),
('product_core_purchase', 'Product Core Purchase'),
])
# Portal Methods
def _portal_try_create(self, request_user, res_id, **kw):
if self.usage == 'product_core_sale':
prefix = 'product_'
product_map = {int(key[len(prefix):]): float(kw[key]) for key in kw if key.find(prefix) == 0 and kw[key]}
if product_map:
service_lines = self._get_product_core_sale_service_lines(request_user.partner_id)
eligible_service_lines = self._product_core_eligible_service_lines(service_lines)
eligible_lines = self._rma_product_core_eligible_data(service_lines, eligible_service_lines)
lines = []
for product_id, qty in product_map.items():
product_f = filter(lambda key_product: key_product.id == product_id, eligible_lines)
if product_f:
product = next(product_f)
product_data = eligible_lines[product]
if not qty:
continue
if qty < 0.0 or product_data['qty_delivered'] < qty:
raise ValidationError('Invalid quantity.')
lines.append((0, 0, {
'product_id': product.id,
'product_uom_id': product.uom_id.id,
'product_uom_qty': qty,
}))
if not lines:
raise ValidationError('Missing product quantity.')
rma = self.env['rma.rma'].create({
'name': _('New'),
'template_id': self.id,
'partner_id': request_user.partner_id.id,
'partner_shipping_id': request_user.partner_id.id,
'lines': lines,
})
return rma
return super(RMATemplate, self)._portal_try_create(request_user, res_id, **kw)
def _portal_template(self, res_id=None):
if self.usage == 'product_core_sale':
return 'rma_product_cores.portal_new'
return super(RMATemplate, self)._portal_template(res_id=res_id)
def _portal_values(self, request_user, res_id=None):
if self.usage == 'product_core_sale':
service_lines = self._get_product_core_sale_service_lines(request_user.partner_id)
eligible_service_lines = self._product_core_eligible_service_lines(service_lines)
eligible_lines = self._rma_product_core_eligible_data(service_lines, eligible_service_lines)
return {
'rma_template': self,
'rma_product_core_lines': eligible_lines,
}
return super(RMATemplate, self)._portal_values(request_user, res_id=res_id)
# Product Cores
def _get_product_core_sale_service_lines(self, partner):
return partner.sale_order_ids.mapped('order_line').filtered(lambda l: l.core_line_id)\
.sorted(key=lambda r: r.id)
def _product_core_eligible_service_lines(self, service_lines, date=None):
if not date:
date = fields.Datetime.now()
lines = service_lines.browse()
for line in service_lines:
validity = line.core_line_id.product_id.product_core_validity
partition_date = date - relativedelta(days=validity)
if line.order_id.date_order >= partition_date:
lines += line
return lines
def _rma_product_core_eligible_data(self, service_lines, eligible_service_lines):
rma_model = self.env['rma.rma']
eligible_lines = defaultdict(lambda: {
'qty_ordered': 0.0,
'qty_delivered': 0.0,
'qty_invoiced': 0.0,
'lines': self.env['sale.order.line'].browse()})
for line in service_lines:
product = rma_model._get_dirty_core_from_service_line(line)
if product:
eligible_line = eligible_lines[product]
eligible_line['lines'] += line
if line in eligible_service_lines:
eligible_line['qty_ordered'] += line.product_uom_qty
eligible_line['qty_delivered'] += line.qty_delivered
eligible_line['qty_invoiced'] += line.qty_invoiced
return eligible_lines
class RMA(models.Model):
_inherit = 'rma.rma'
def action_done(self):
res = super(RMA, self).action_done()
res2 = self._product_core_action_done()
if isinstance(res, dict) and isinstance(res2, dict):
if 'warning' in res and 'warning' in res2:
res['warning'] = '\n'.join([res['warning'], res2['warning']])
return res
if 'warning' in res2:
res['warning'] = res2['warning']
return res
elif isinstance(res2, dict):
return res2
return res
def _get_dirty_core_from_service_line(self, line):
original_product_line = line.core_line_id
return original_product_line.product_id.product_core_id
def _product_core_action_done(self):
for rma in self.filtered(lambda r: r.template_usage in ('product_core_sale', 'product_core_purchase')):
if rma.template_usage == 'product_core_sale':
service_lines = rma.template_id._get_product_core_sale_service_lines(rma.partner_id)
eligible_service_lines = rma.template_id._product_core_eligible_service_lines(service_lines, date=rma.create_date)
# collect the to reduce qty by product id
qty_to_reduce = defaultdict(float)
for line in rma.lines:
qty_to_reduce[line.product_id.id] += line.product_uom_qty
# iterate over all service_lines to see if the qty_delivered can be reduced.
sale_orders = self.env['sale.order'].browse()
for line in eligible_service_lines:
product = self._get_dirty_core_from_service_line(line)
pid = product.id
if qty_to_reduce[pid] > 0.0:
if line.qty_delivered > 0.0:
sale_orders += line.order_id
if qty_to_reduce[pid] > line.qty_delivered:
# can reduce this whole line
qty_to_reduce[pid] -= line.qty_delivered
line.write({'qty_delivered': 0.0})
else:
# can reduce some of this line, but there are no more to reduce
line.write({'qty_delivered': line.qty_delivered - qty_to_reduce[pid]})
qty_to_reduce[pid] = 0.0
# if there are more qty to reduce, then we have an error.
if any(qty_to_reduce.values()):
raise UserError(_('Cannot complete RMA as there are not enough service lines to reduce. (Maybe a duplicate)'))
# Try to invoice if we don't already have an invoice (e.g. from resetting to draft)
if sale_orders and rma.template_id.invoice_done and not rma.invoice_ids:
rma.invoice_ids += rma._product_core_sale_invoice_done(sale_orders)
else:
raise UserError(_('not ready for purchase rma'))
return True
def _product_core_sale_invoice_done(self, sale_orders):
return self._sale_invoice_done(sale_orders)
def action_add_product_core_lines(self):
make_line_obj = self.env['rma.product_cores.make.lines']
for rma in self:
lines = make_line_obj.create({
'rma_id': rma.id,
})
action = self.env.ref('rma_product_cores.action_rma_add_lines').read()[0]
action['res_id'] = lines.id
return action
def _product_cores_create_make_lines(self, wizard, rma_line_model):
"""
Called from the wizard, as this model "owns" the eligibility and qty on lines.
:param wizard:
:param line_model:
:return:
"""
if self.partner_id:
if self.template_usage == 'product_core_sale':
service_lines = self.template_id._get_product_core_sale_service_lines(self.partner_id)
eligible_service_lines = self.template_id._product_core_eligible_service_lines(service_lines, date=self.create_date)
rma_lines = rma_line_model.browse()
for line in service_lines:
product = self._get_dirty_core_from_service_line(line)
if product:
rma_line = rma_lines.filtered(lambda l: l.product_id == product)
if not rma_line:
rma_line = rma_line_model.create({
'rma_make_lines_id': wizard.id,
'product_id': product.id,
'product_uom_id': product.uom_id.id,
})
rma_lines += rma_line
if line in eligible_service_lines:
rma_line.update({
'qty_ordered': rma_line.qty_ordered + line.product_uom_qty,
'qty_delivered': rma_line.qty_delivered + line.qty_delivered,
'qty_invoiced': rma_line.qty_invoiced + line.qty_invoiced,
})
elif self.template_usage == 'product_core_purchase':
raise UserError('not ready for purchase rma')
def _product_core_field_check(self):
if not self.partner_shipping_id:
raise UserError(_('You must have a shipping address selected for this RMA.'))
if not self.partner_id:
raise UserError(_('You must have a partner selected for this RMA.'))
def _create_in_picking_product_core_sale(self):
self._product_core_field_check()
values = self.template_id._values_for_in_picking(self)
return self._picking_from_values(values, {}, {})
def _create_out_picking_product_core_sale(self):
self._product_core_field_check()
values = self.template_id._values_for_out_picking(self)
return self._picking_from_values(values, {}, {})
def _create_in_picking_product_core_purchase(self):
self._product_core_field_check()
values = self.template_id._values_for_in_picking(self)
return self._picking_from_values(values, {}, {})
def _create_out_picking_product_core_purchase(self):
self._product_core_field_check()
values = self.template_id._values_for_out_picking(self)
return self._picking_from_values(values, {}, {})

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import test_rma

View File

@@ -0,0 +1,144 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.addons.rma.tests.test_rma import TestRMA
from odoo.exceptions import UserError, ValidationError
from dateutil.relativedelta import relativedelta
class TestRMACore(TestRMA):
def setUp(self):
super(TestRMACore, self).setUp()
self.template_sale_return = self.env.ref('rma_product_cores.template_product_core_sale_return')
self.template_purchase_return = self.env.ref('rma_product_cores.template_product_core_purchase_return')
self.product_core_service = self.env['product.product'].create({
'name': 'Turbo Core Deposit',
'type': 'service',
'categ_id': self.env.ref('product.product_category_all').id,
'core_ok': True,
'invoice_policy': 'delivery',
})
self.product_core = self.env['product.product'].create({
'name': 'Turbo Core',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
'core_ok': True,
'tracking': 'serial',
'invoice_policy': 'delivery',
})
def test_30_product_core_sale_return(self):
# Initialize template
self.template_sale_return.usage = 'product_core_sale'
self.template_sale_return.invoice_done = True
self.product1.tracking = 'serial'
self.product1.product_core_id = self.product_core
self.product1.product_core_service_id = self.product_core_service
self.product1.product_core_validity = 30 # eligible for 30 days
order = self.env['sale.order'].create({
'partner_id': self.partner1.id,
'partner_invoice_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
'order_line': [(0, 0, {
'product_id': self.product1.id,
'product_uom_qty': 1.0,
'product_uom': self.product1.uom_id.id,
'price_unit': 10.0,
})]
})
order.action_confirm()
original_date_order = order.date_order
self.assertTrue(order.state in ('sale', 'done'))
self.assertEqual(len(order.picking_ids), 1, 'Tests only run with single stage delivery.')
# Try to RMA item not delivered yet
rma = self.env['rma.rma'].create({
'template_id': self.template_sale_return.id,
'partner_id': self.partner1.id,
'partner_shipping_id': self.partner1.id,
})
self.assertEqual(rma.state, 'draft')
wizard = self.env['rma.product_cores.make.lines'].create({
'rma_id': rma.id,
})
self.assertEqual(wizard.line_ids.qty_delivered, 0.0)
wizard.line_ids.product_uom_qty = 1.0
with self.assertRaises(UserError):
# Prevents adding if the qty_delivered on the line is not >= product_uom_qty
wizard.add_lines()
order.picking_ids.action_assign()
pack_opt = order.picking_ids.move_line_ids[0]
lot = self.env['stock.production.lot'].create({
'product_id': self.product1.id,
'name': 'X100',
'product_uom_id': self.product1.uom_id.id,
'company_id': self.env.user.company_id.id,
})
pack_opt.qty_done = 1.0
pack_opt.lot_id = lot
order.picking_ids.button_validate()
self.assertEqual(order.picking_ids.state, 'done')
self.assertEqual(order.order_line.filtered(lambda l: l.product_id == self.product1).qty_delivered,
1.0)
self.assertEqual(order.order_line.filtered(lambda l: l.product_id == self.product_core_service).qty_delivered,
1.0)
# ensure that we have a qty_delivered
wizard = self.env['rma.product_cores.make.lines'].create({
'rma_id': rma.id,
})
self.assertEqual(wizard.line_ids.qty_delivered, 1.0)
# set the date back and ensure that we have 0 again.
order.date_order = order.date_order - relativedelta(days=31)
wizard = self.env['rma.product_cores.make.lines'].create({
'rma_id': rma.id,
})
self.assertEqual(wizard.line_ids.qty_delivered, 0.0)
# Reset Date and process RMA
order.date_order = original_date_order
wizard = self.env['rma.product_cores.make.lines'].create({
'rma_id': rma.id,
})
self.assertEqual(wizard.line_ids.qty_delivered, 1.0)
wizard.line_ids.product_uom_qty = 1.0
wizard.add_lines()
# Invoice the order so that only the core product is invoiced at the end...
self.assertFalse(order.invoice_ids)
wiz = self.env['sale.advance.payment.inv'].with_context(active_ids=order.ids).create({})
wiz.create_invoices()
order.flush()
self.assertTrue(order.invoice_ids)
# The added product should be the 'dirty core' for the RMA's `core_product_id`
self.assertEqual(rma.lines.product_id, self.product1.product_core_id)
rma.action_confirm()
self.assertTrue(rma.in_picking_id)
self.assertEqual(rma.in_picking_id.state, 'assigned')
pack_opt = rma.in_picking_id.move_line_ids[0]
pack_opt.lot_id = self.env['stock.production.lot'].create({
'product_id': pack_opt.product_id.id,
'name': 'TESTDIRTYLOT1',
'company_id': self.env.user.company_id.id,
})
pack_opt.qty_done = 1.0
rma.in_picking_id.button_validate()
rma.action_done()
self.assertEqual(rma.state, 'done')
# Finishing the RMA should have made an invoice
self.assertTrue(rma.invoice_ids, 'Finishing RMA did not create an invoice(s).')
self.assertEqual(rma.invoice_ids.invoice_line_ids.product_id, self.product_core_service)
# Make sure the delivered qty of the Core Service was decremented.
self.assertEqual(order.order_line.filtered(lambda l: l.product_id == self.product1).qty_delivered,
1.0)
# This is known to work in practice, but no amount of magic ORM flushing seems to make it work in test.
# self.assertEqual(order.order_line.filtered(lambda l: l.product_id == self.product_core_service).qty_delivered,
# 0.0)

View File

@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- New -->
<template id="portal_new" name="New Product Core RMA">
<t t-call="portal.portal_layout">
<div id="optional_placeholder"></div>
<div class="container">
<t t-call="rma.portal_rma_error"/>
<div class="card">
<div class="card-header">
<div class="row">
<div class="col-lg-12">
<h4>
<span t-esc="rma_template.name"/>
</h4>
</div>
</div>
</div>
<div class="card-body">
<p t-if="not rma_product_core_lines">No product cores in your purchase history.</p>
<form t-if="rma_product_core_lines" method="post" t-attf-action="/my/rma/new/#{rma_template.id}/res/1">
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="row">
<div class="col-lg-5">
<strong>Product</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Qty. Ordered</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Qty. Invoiced</strong>
</div>
<div class="col-lg-2 text-right">
<strong>Qty. Delivered</strong>
</div>
<div class="col-lg-1 text-right">
<strong>Qty. to Return</strong>
</div>
</div>
<t t-foreach="rma_product_core_lines" t-as="product">
<t t-set="core_lines" t-value="rma_product_core_lines[product]"/>
<hr/>
<div class="row">
<div class="col-lg-1 text-center">
<img t-attf-src="/web/image/product.product/#{product.id}/image_64"
width="64" alt="Product image"></img>
</div>
<div class="col-lg-4">
<span t-esc="product.name"/>
<button class="btn btn-secondary btn-sm float-right" type="button" data-toggle="collapse" t-attf-data-target="#product_detail_#{product.id}" aria-expanded="false" aria-controls="collapseExample">
<span class="fa fa-info-circle"/>
</button>
</div>
<div class="col-lg-2 text-right">
<span t-esc="core_lines['qty_ordered']"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="core_lines['qty_invoiced']"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="core_lines['qty_delivered']"/>
</div>
<div class="col-lg-1 text-right">
<input type="text" t-attf-name="product_#{product.id}" class="form-control"/>
</div>
</div>
<div class="collapse text-muted" t-attf-id="product_detail_#{product.id}">
<div class="row" t-foreach="core_lines['lines']" t-as="l">
<t t-set="validity" t-value="float(l.core_line_id.product_id.product_core_validity)"/>
<t t-set="partition_date" t-value="current_date - relativedelta(days=validity)"/>
<div class="col-lg-3">
<span t-field="l.order_id"/>
</div>
<div class="col-lg-2">
<span t-field="l.order_id.date_order"
t-attf-class="#{'text-danger' if l.order_id.date_order &lt; partition_date else 'text-success'}"
t-options='{"widget": "date"}'/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="l.product_uom_qty"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="l.qty_invoiced"/>
</div>
<div class="col-lg-2 text-right">
<span t-esc="l.qty_delivered"/>
</div>
</div>
</div>
</t>
<input type="submit" class="btn btn-primary mt16 float-right" name="submit"/>
</form>
</div>
</div>
</div>
<div class="oe_structure mb32"/>
</t>
</template>
</odoo>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- RMA -->
<record id="view_rma_rma_form_product_cores" model="ir.ui.view">
<field name="name">rma.rma.form.product_cores</field>
<field name="model">rma.rma</field>
<field name="inherit_id" ref="rma.view_rma_rma_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='template_id']" position="after">
<br/>
<button string="Add lines" type="object" name="action_add_product_core_lines" attrs="{'invisible': ['|', ('template_usage', 'not in', ('product_core_sale')), ('state', '!=', 'draft')]}"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import rma_lines

View File

@@ -0,0 +1,43 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
from odoo.exceptions import UserError
class RMAProductCoresMakeLines(models.TransientModel):
_name = 'rma.product_cores.make.lines'
_description = 'Add Product Core Lines'
rma_id = fields.Many2one('rma.rma', string='RMA')
line_ids = fields.One2many('rma.product_cores.make.lines.line', 'rma_make_lines_id', string='Lines')
@api.model
def create(self, vals):
maker = super(RMAProductCoresMakeLines, self).create(vals)
maker._create_lines()
return maker
def _create_lines(self):
make_lines_obj = self.env['rma.product_cores.make.lines.line']
self.rma_id._product_cores_create_make_lines(self, make_lines_obj)
def add_lines(self):
rma_line_obj = self.env['rma.line']
for o in self:
lines = o.line_ids.filtered(lambda l: l.product_uom_qty > 0.0)
for l in lines:
if l.qty_delivered < l.product_uom_qty:
raise UserError('You cannot return more than the eligible qty of this product.')
rma_line_obj.create({
'rma_id': o.rma_id.id,
'product_id': l.product_id.id,
'product_uom_id': l.product_uom_id.id,
'product_uom_qty': l.product_uom_qty,
})
class RMAProductCoresMakeLinesLine(models.TransientModel):
_name = 'rma.product_cores.make.lines.line'
_inherit = 'rma.sale.make.lines.line'
rma_make_lines_id = fields.Many2one('rma.product_cores.make.lines')

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="view_rma_add_lines_form" model="ir.ui.view">
<field name="name">view.rma.add.lines.form</field>
<field name="model">rma.product_cores.make.lines</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form>
<field name="line_ids">
<tree editable="top" create="false" delete="false">
<field name="product_id" readonly="1"/>
<field name="qty_ordered" readonly="1"/>
<field name="qty_delivered" readonly="1"/>
<field name="qty_invoiced" readonly="1"/>
<field name="product_uom_qty"/>
<field name="product_uom_id" readonly="1"/>
</tree>
</field>
<footer>
<button class="oe_highlight"
name="add_lines"
type="object"
string="Add" />
<button class="oe_link"
special="cancel"
string="Cancel" />
</footer>
</form>
</field>
</record>
<record id="action_rma_add_lines" model="ir.actions.act_window">
<field name="name">Add RMA Lines</field>
<field name="res_model">rma.product_cores.make.lines</field>
<field name="view_mode">form</field>
<field name="view_id" ref="view_rma_add_lines_form" />
<field name="target">new</field>
</record>
</odoo>