[MOV] product_cores: from Hibou Suite Enterprise for 13.0

This commit is contained in:
Jared Kipe
2020-07-03 09:12:28 -07:00
parent 11ee6a567d
commit e287e08483
11 changed files with 573 additions and 0 deletions

View File

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

25
product_cores/__manifest__.py Executable file
View File

@@ -0,0 +1,25 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'Product Cores',
'author': 'Hibou Corp. <hello@hibou.io>',
'version': '13.0.1.0.0',
'category': 'Tools',
'license': 'OPL-1',
'summary': 'Charge customers core deposits.',
'description': """
Charge customers core deposits.
""",
'website': 'https://hibou.io/',
'depends': [
'sale_stock',
'purchase',
],
'data': [
'views/product_views.xml',
'views/purchase_views.xml',
'views/sale_views.xml',
],
'installable': True,
'application': False,
}

View File

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

View File

@@ -0,0 +1,61 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models
class ProductTemplate(models.Model):
_inherit = 'product.template'
core_ok = fields.Boolean(string='Core')
product_core_service_id = fields.Many2one('product.product', string='Product Core Deposit',
compute='_compute_product_core_service',
inverse='_set_product_core_service')
product_core_id = fields.Many2one('product.product', string='Product Core',
compute='_compute_product_core',
inverse='_set_product_core')
product_core_validity = fields.Integer(string='Product Core Return Validity',
help='How long after a sale the core is eligible for return. (in days)')
@api.depends('product_variant_ids', 'product_variant_ids.product_core_service_id')
def _compute_product_core_service(self):
unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1)
for template in unique_variants:
template.product_core_service_id = template.product_variant_ids.product_core_service_id
for template in (self - unique_variants):
template.product_core_service_id = False
@api.depends('product_variant_ids', 'product_variant_ids.product_core_id')
def _compute_product_core(self):
unique_variants = self.filtered(lambda template: len(template.product_variant_ids) == 1)
for template in unique_variants:
template.product_core_id = template.product_variant_ids.product_core_id
for template in (self - unique_variants):
template.product_core_id = False
def _set_product_core_service(self):
if len(self.product_variant_ids) == 1:
self.product_variant_ids.product_core_service_id = self.product_core_service_id
def _set_product_core(self):
if len(self.product_variant_ids) == 1:
self.product_variant_ids.product_core_id = self.product_core_id
class ProductProduct(models.Model):
_inherit = 'product.product'
product_core_service_id = fields.Many2one('product.product', string='Product Core Deposit')
product_core_id = fields.Many2one('product.product', string='Product Core')
def get_purchase_core_service(self, vendor):
seller_line = self.seller_ids.filtered(lambda l: l.name == vendor and l.product_core_service_id)
# only want to return the first one
for l in seller_line:
return l.product_core_service_id
return seller_line
class ProductSupplierinfo(models.Model):
_inherit = 'product.supplierinfo'
product_core_service_id = fields.Many2one('product.product', string='Product Core Deposit')

View File

@@ -0,0 +1,64 @@
# 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 PurchaseOrder(models.Model):
_inherit = 'purchase.order'
def copy(self, default=None):
new_po = super(PurchaseOrder, self).copy(default=default)
for line in new_po.order_line.filtered(lambda l: l.product_id.core_ok and not l.core_line_id):
line.unlink()
return new_po
class PurchaseOrderLine(models.Model):
_inherit = 'purchase.order.line'
core_line_id = fields.Many2one('purchase.order.line', string='Core Purchase Line', copy=False)
@api.model
def create(self, values):
res = super(PurchaseOrderLine, self).create(values)
other_product = res.product_id.get_purchase_core_service(res.order_id.partner_id)
if other_product:
values['product_id'] = other_product.id
values['name'] = other_product.name
values['price_unit'] = other_product.list_price
values['core_line_id'] = res.id
other_line = self.create(values)
other_line._compute_tax_id()
return res
def write(self, values):
res = super(PurchaseOrderLine, self).write(values)
if 'product_id' in values or 'product_qty' in values or 'product_uom' in values:
self.filtered(lambda l: not l.core_line_id)\
.mapped('order_id.order_line')\
.filtered('core_line_id')\
._update_core_line()
return res
def unlink(self):
for line in self:
if line.core_line_id and line.core_line_id and not self.env.user.has_group('purchase.group_purchase_user'):
raise UserError(_('You cannot delete a core line while the original still exists.'))
# Unlink any linked core lines.
other_line = line.order_id.order_line.filtered(lambda l: l.core_line_id == line)
if other_line and other_line not in self:
other_line.write({'core_line_id': False})
other_line.unlink()
return super(PurchaseOrderLine, self).unlink()
def _update_core_line(self):
for line in self:
if line.core_line_id and line.core_line_id.product_id.get_purchase_core_service(line.order_id.partner_id):
line.update({
'product_id': line.core_line_id.product_id.get_purchase_core_service(line.order_id.partner_id).id,
'product_qty': line.core_line_id.product_qty,
'product_uom': line.core_line_id.product_uom.id,
})
elif line.core_line_id:
line.unlink()

View File

@@ -0,0 +1,68 @@
# 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 SaleOrder(models.Model):
_inherit = 'sale.order'
def copy(self, default=None):
new_so = super(SaleOrder, self).copy(default=default)
for line in new_so.order_line.filtered(lambda l: l.product_id.core_ok and not l.core_line_id):
line.unlink()
return new_so
class SaleOrderLine(models.Model):
_inherit = 'sale.order.line'
core_line_id = fields.Many2one('sale.order.line', string='Core Sale Line', copy=False)
@api.model
def create(self, values):
res = super(SaleOrderLine, self).create(values)
if res.product_id.product_core_service_id:
other_product = res.product_id.product_core_service_id
values['product_id'] = other_product.id
values['name'] = other_product.name
values['price_unit'] = other_product.list_price
values['core_line_id'] = res.id
other_line = self.create(values)
other_line._compute_tax_id()
return res
def write(self, values):
res = super(SaleOrderLine, self).write(values)
if 'product_id' in values or 'product_uom_qty' in values or 'product_uom' in values:
for line in self.filtered(lambda l: not l.core_line_id):
line.mapped('order_id.order_line').filtered(lambda l: l.core_line_id == line)._update_core_line()
return res
def unlink(self):
for line in self:
if line.core_line_id and line.core_line_id and not self.env.user.has_group('sales_team.group_sale_manager'):
raise UserError(_('You cannot delete a core line while the original still exists.'))
# Unlink any linked core lines.
other_line = line.order_id.order_line.filtered(lambda l: l.core_line_id == line)
if other_line and other_line not in self:
other_line.write({'core_line_id': False})
other_line.unlink()
return super(SaleOrderLine, self).unlink()
def _update_core_line(self):
for line in self:
if line.core_line_id and line.core_line_id.product_id.product_core_service_id:
line.write({
'product_id': line.core_line_id.product_id.product_core_service_id.id,
'product_uom_qty': line.core_line_id.product_uom_qty,
'product_uom': line.core_line_id.product_uom.id,
})
elif line.core_line_id:
line.unlink()
@api.depends('core_line_id.qty_delivered')
def _compute_qty_delivered(self):
super(SaleOrderLine, self)._compute_qty_delivered()
for line in self.filtered(lambda l: l.core_line_id):
line.qty_delivered = line.core_line_id.qty_delivered

View File

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

View File

@@ -0,0 +1,236 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.tests import common, Form
from odoo import fields
class TestProductCores(common.TransactionCase):
def setUp(self):
super(TestProductCores, self).setUp()
self.customer = self.env.ref('base.res_partner_2')
self.vendor = self.env.ref('base.res_partner_12')
self.purchase_tax_physical = self.env['account.tax'].create({
'name': 'Purchase Tax Physical',
'type_tax_use': 'purchase',
'amount': 5.0,
})
self.purchase_tax_service = self.env['account.tax'].create({
'name': 'Purchase Tax Service',
'type_tax_use': 'purchase',
'amount': 1.0,
})
self.sale_tax_physical = self.env['account.tax'].create({
'name': 'Sale Tax Physical',
'type_tax_use': 'sale',
'amount': 5.0,
})
self.sale_tax_service = self.env['account.tax'].create({
'name': 'Sale Tax Service',
'type_tax_use': 'sale',
'amount': 1.0,
})
self.product = self.env['product.product'].create({
'name': 'Turbo',
'type': 'product',
'categ_id': self.env.ref('product.product_category_all').id,
'supplier_taxes_id': [(6, 0, [self.purchase_tax_physical.id])],
'taxes_id': [(6, 0, [self.sale_tax_physical.id])]
})
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,
'service_type': 'manual',
'supplier_taxes_id': [(6, 0, [self.purchase_tax_service.id])],
'taxes_id': [(6, 0, [self.sale_tax_service.id])]
})
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,
})
self.product.product_core_id = self.product_core
self.product.product_core_service_id = self.product_core_service
def test_01_purchase(self):
purchase = self.env['purchase.order'].create({
'partner_id': self.vendor.id,
})
test_qty = 2.0
po_line = self.env['purchase.order.line'].create({
'order_id': purchase.id,
'product_id': self.product.id,
'name': 'Test',
'date_planned': purchase.date_order,
'product_qty': test_qty,
'product_uom': self.product.uom_id.id,
'price_unit': 10.0,
})
# Compute taxes since it didn't come from being created
po_line._compute_tax_id()
# No service line should have been created.
self.assertEqual(len(purchase.order_line), 1)
po_line.unlink()
# Create a supplierinfo for this vendor with a core service product
self.env['product.supplierinfo'].create({
'name': self.vendor.id,
'price': 10.0,
'product_core_service_id': self.product_core_service.id,
'product_tmpl_id': self.product.product_tmpl_id.id,
})
po_line = self.env['purchase.order.line'].create({
'order_id': purchase.id,
'product_id': self.product.id,
'name': 'Test',
'date_planned': purchase.date_order,
'product_qty': test_qty,
'product_uom': self.product.uom_id.id,
'price_unit': 10.0,
})
po_line._compute_tax_id()
# Ensure second line was created
self.assertEqual(len(purchase.order_line), 2)
# Ensure second line has the same quantity
self.assertTrue(all(l.product_qty == test_qty for l in purchase.order_line))
po_line_service = purchase.order_line.filtered(lambda l: l.product_id == self.product_core_service)
self.assertTrue(po_line_service)
# Ensure correct taxes
self.assertEqual(po_line.taxes_id, self.purchase_tax_physical)
self.assertEqual(po_line_service.taxes_id, self.purchase_tax_service)
test_qty = 10.0
po_line.product_qty = test_qty
# Ensure second line has the same quantity
self.assertTrue(all(l.product_qty == test_qty for l in purchase.order_line))
purchase.button_confirm()
self.assertEqual(purchase.state, 'purchase')
self.assertEqual(len(purchase.picking_ids), 1)
purchase.picking_ids.button_validate()
# From purchase.tests.test_purchase_order_report in 13
f = Form(self.env['account.move'].with_context(default_type='in_invoice'))
f.partner_id = purchase.partner_id
f.purchase_id = purchase
vendor_bill = f.save()
self.assertEqual(len(vendor_bill.invoice_line_ids), 2)
vendor_bill.post()
purchase.flush()
# Duplicate PO
# Should not 'duplicate' the original service line
purchase2 = purchase.copy()
self.assertEqual(len(purchase2.order_line), 2)
po_line2 = purchase2.order_line.filtered(lambda l: l.product_id == self.product)
po_line_service2 = purchase2.order_line.filtered(lambda l: l.product_id == self.product_core_service)
self.assertTrue(po_line2)
self.assertTrue(po_line_service2)
# Should not be allowed to remove the service line.
# Managers can remove the service line.
# with self.assertRaises(UserError):
# po_line_service2.unlink()
po_line2.unlink()
# Deleting the main product line should delete the service line
self.assertFalse(po_line2.exists())
self.assertFalse(po_line_service2.exists())
def test_02_sale(self):
# Need Inventory.
adjustment = self.env['stock.inventory'].create({
'name': 'Initial',
'product_ids': [(4, self.product.id)],
})
adjustment.action_start()
if not adjustment.line_ids:
adjustment_line = self.env['stock.inventory.line'].create({
'inventory_id': adjustment.id,
'product_id': self.product.id,
'location_id': self.env.ref('stock.warehouse0').lot_stock_id.id,
})
adjustment.line_ids.write({
# Maybe add Serial.
'product_qty': 20.0,
})
adjustment.action_validate()
sale = self.env['sale.order'].create({
'partner_id': self.customer.id,
'date_order': fields.Datetime.now(),
'picking_policy': 'direct',
})
test_qty = 2.0
so_line = self.env['sale.order.line'].create({
'order_id': sale.id,
'product_id': self.product.id,
'name': 'Test',
'product_uom_qty': test_qty,
'product_uom': self.product.uom_id.id,
'price_unit': 10.0,
})
# Compute taxes since it didn't come from being created
so_line._compute_tax_id()
# Ensure second line was created
self.assertEqual(len(sale.order_line), 2)
# Ensure second line has the same quantity
self.assertTrue(all(l.product_uom_qty == test_qty for l in sale.order_line))
so_line_service = sale.order_line.filtered(lambda l: l.product_id == self.product_core_service)
self.assertTrue(so_line_service)
# Ensure correct taxes
self.assertEqual(so_line.tax_id, self.sale_tax_physical)
self.assertEqual(so_line_service.tax_id, self.sale_tax_service)
test_qty = 1.0
so_line.product_uom_qty = test_qty
# Ensure second line has the same quantity
self.assertTrue(all(l.product_qty == test_qty for l in sale.order_line))
sale.action_confirm()
self.assertTrue(sale.state in ('sale', 'done'))
self.assertEqual(len(sale.picking_ids), 1)
self.assertEqual(len(sale.picking_ids.move_lines), 1)
self.assertEqual(sale.picking_ids.move_lines.product_id, self.product)
sale.picking_ids.action_assign()
self.assertEqual(so_line.product_uom_qty, sale.picking_ids.move_lines.reserved_availability)
res_dict = sale.picking_ids.button_validate()
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
wizard.process()
self.assertEqual(so_line.qty_delivered, so_line.product_uom_qty)
# Ensure all products are delivered.
self.assertTrue(all(l.product_qty == l.qty_delivered for l in sale.order_line))
# Duplicate SO
# Should not 'duplicate' the original service line
sale2 = sale.copy()
self.assertEqual(len(sale2.order_line), 2)
so_line2 = sale2.order_line.filtered(lambda l: l.product_id == self.product)
so_line_service2 = sale2.order_line.filtered(lambda l: l.product_id == self.product_core_service)
self.assertTrue(so_line2)
self.assertTrue(so_line_service2)
# Should not be allowed to remove the service line.
# Managers can remove the service line
# with self.assertRaises(UserError):
# so_line_service2.unlink()
so_line2.unlink()
# Deleting the main product line should delete the service line
self.assertFalse(so_line2.exists())
self.assertFalse(so_line_service2.exists())
# Return the SO
self.assertEqual(len(sale.picking_ids), 1)
wiz = self.env['stock.return.picking'].with_context(active_model='stock.picking', active_id=sale.picking_ids.id).create({})
wiz._onchange_picking_id()
wiz.create_returns()
self.assertEqual(len(sale.picking_ids), 2)
return_picking = sale.picking_ids.filtered(lambda p: p.state != 'done')
self.assertTrue(return_picking)
res_dict = return_picking.button_validate()
wizard = self.env[(res_dict.get('res_model'))].browse(res_dict.get('res_id'))
wizard.process()
self.assertTrue(all(l.qty_delivered == 0.0 for l in sale.order_line))

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="product_template_only_form_view_inherit" model="ir.ui.view">
<field name="name">product.template.product.form.inherit</field>
<field name="model">product.template</field>
<field name="inherit_id" ref="product.product_template_only_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='options']" position="inside">
<div>
<field name="core_ok"/>
<label for="core_ok"/>
</div>
</xpath>
<xpath expr="//page[@name='inventory']" position="inside">
<group name="cores" string="Cores" attrs="{'invisible': [('product_variant_count', '&gt;', 1)]}">
<field name="product_core_service_id" domain="[('product_tmpl_id.core_ok', '=', True)]" context="{'default_core_ok': True}"/>
<field name="product_core_id" domain="[('product_tmpl_id.core_ok', '=', True)]" context="{'default_core_ok': True}"/>
<field name="product_core_validity"/>
</group>
</xpath>
</field>
</record>
<record id="product_normal_form_view_inherit" model="ir.ui.view">
<field name="name">product.product.form.inherit</field>
<field name="model">product.product</field>
<field name="inherit_id" ref="product.product_normal_form_view"/>
<field name="arch" type="xml">
<xpath expr="//div[@name='options']" position="inside">
<div>
<field name="core_ok"/>
<label for="core_ok"/>
</div>
</xpath>
<xpath expr="//page[@name='inventory']" position="inside">
<group name="cores" string="Cores">
<field name="product_core_service_id" domain="[('product_tmpl_id.core_ok', '=', True)]" context="{'default_core_ok': True}"/>
<field name="product_core_id" domain="[('product_tmpl_id.core_ok', '=', True)]" context="{'default_core_ok': True}"/>
<field name="product_core_validity"/>
</group>
</xpath>
</field>
</record>
<!-- product.supplierinfo -->
<record id="product_supplierinfo_tree_view_inherit" model="ir.ui.view">
<field name="name">product.supplierinfo.tree.view.inherit</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='name']" position="after">
<field name="product_core_service_id"/>
</xpath>
</field>
</record>
<record id="product_supplierinfo_form_view_inherit" model="ir.ui.view">
<field name="name">product.supplierinfo.form.view.inherit</field>
<field name="model">product.supplierinfo</field>
<field name="inherit_id" ref="product.product_supplierinfo_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='product_id']" position="after">
<field name="product_core_service_id"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="purchase_order_form_inherit" model="ir.ui.view">
<field name="name">purchase.order.form.inherit</field>
<field name="model">purchase.order</field>
<field name="inherit_id" ref="purchase.purchase_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']/tree" position="inside">
<field name="core_line_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='product_id']" position="after">
<field name="core_line_id" readonly="1" force_save="1"/>
</xpath>
</field>
</record>
</odoo>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_order_form_inherit" model="ir.ui.view">
<field name="name">sale.order.form.inherit</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='order_line']/tree" position="inside">
<field name="core_line_id" invisible="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/form//field[@name='product_id']" position="after">
<field name="core_line_id" readonly="1" force_save="1"/>
</xpath>
<xpath expr="//field[@name='order_line']/kanban/field[@name='product_id']" position="after">
<field name="core_line_id"/>
</xpath>
</field>
</record>
</odoo>