mirror of
https://gitlab.com/hibou-io/hibou-odoo/suite.git
synced 2025-01-20 12:37:31 +02:00
Merge branch 'mig/15.0/product_cores' into '15.0'
mig/15.0/product_cores into 15.0 See merge request hibou-io/hibou-odoo/suite!1208
This commit is contained in:
3
product_cores/__init__.py
Normal file
3
product_cores/__init__.py
Normal 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
25
product_cores/__manifest__.py
Executable 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': '15.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_stock',
|
||||
],
|
||||
'data': [
|
||||
'views/product_views.xml',
|
||||
'views/purchase_views.xml',
|
||||
'views/sale_views.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
6
product_cores/models/__init__.py
Normal file
6
product_cores/models/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from . import account
|
||||
from . import product
|
||||
from . import purchase
|
||||
from . import sale
|
||||
41
product_cores/models/account.py
Normal file
41
product_cores/models/account.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
|
||||
|
||||
from datetime import timedelta
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountMove(models.Model):
|
||||
_inherit = 'account.move'
|
||||
|
||||
def _post(self, soft=True):
|
||||
if self._context.get('move_reverse_cancel'):
|
||||
return super(AccountMove, self)._post(soft)
|
||||
self._product_core_set_date_maturity()
|
||||
return super(AccountMove, self)._post(soft)
|
||||
|
||||
def _product_core_set_date_maturity(self):
|
||||
for move in self:
|
||||
for line in move.invoice_line_ids.filtered(lambda l: l.product_id.core_ok and l.product_id.type == 'service'):
|
||||
regular_date_maturity = line.date + timedelta(days=(line.product_id.product_core_validity or 0))
|
||||
if move.move_type in ('in_invoice', 'in_refund', 'in_receipt'):
|
||||
# derive from purchase
|
||||
if move.move_type == 'in_refund' and line.purchase_line_id:
|
||||
# try to date from original
|
||||
po_move_lines = self.search([('purchase_line_id', '=', line.purchase_line_id.id)])
|
||||
po_move_lines = po_move_lines.filtered(lambda l: l.move_id.move_type == 'in_invoice')
|
||||
if po_move_lines:
|
||||
line.date_maturity = po_move_lines[0].date_maturity or regular_date_maturity
|
||||
else:
|
||||
line.date_maturity = regular_date_maturity
|
||||
else:
|
||||
line.date_maturity = regular_date_maturity
|
||||
elif move.move_type in ('out_invoice', 'out_refund', 'out_receipt'):
|
||||
# derive from sale
|
||||
if move.move_type == 'out_refund' and line.sale_line_ids:
|
||||
other_move_lines = line.sale_line_ids.mapped('invoice_lines').filtered(lambda l: l.move_id.move_type == 'out_invoice')
|
||||
if other_move_lines:
|
||||
line.date_maturity = other_move_lines[0].date_maturity or regular_date_maturity
|
||||
else:
|
||||
line.date_maturity = regular_date_maturity
|
||||
else:
|
||||
line.date_maturity = regular_date_maturity
|
||||
61
product_cores/models/product.py
Normal file
61
product_cores/models/product.py
Normal 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')
|
||||
71
product_cores/models/purchase.py
Normal file
71
product_cores/models/purchase.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# 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'
|
||||
|
||||
qty_received = fields.Float(recursive=True)
|
||||
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 any(f in values for f in ('product_id', 'product_qty', 'product_uom')):
|
||||
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()
|
||||
|
||||
@api.depends('qty_received_method', 'qty_received_manual', 'core_line_id.qty_received')
|
||||
def _compute_qty_received(self):
|
||||
super(PurchaseOrderLine, self)._compute_qty_received()
|
||||
for line in self.filtered(lambda l: l.qty_received_method == 'manual' and l.core_line_id):
|
||||
line.qty_received = line.core_line_id.qty_received
|
||||
69
product_cores/models/sale.py
Normal file
69
product_cores/models/sale.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# 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'
|
||||
|
||||
qty_delivered = fields.Float(recursive=True)
|
||||
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
|
||||
3
product_cores/tests/__init__.py
Normal file
3
product_cores/tests/__init__.py
Normal 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
|
||||
242
product_cores/tests/test_product_cores.py
Normal file
242
product_cores/tests/test_product_cores.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# 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])],
|
||||
'product_core_validity': 30,
|
||||
})
|
||||
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)
|
||||
self.assertEqual(len(purchase.picking_ids.move_line_ids), 1) # shouldn't have the service
|
||||
purchase.picking_ids.move_line_ids.qty_done = purchase.picking_ids.move_line_ids.product_uom_qty
|
||||
purchase.picking_ids.button_validate()
|
||||
purchase.flush()
|
||||
|
||||
# All lines should be received on the PO
|
||||
for line in purchase.order_line:
|
||||
self.assertEqual(line.product_qty, line.qty_received)
|
||||
|
||||
# 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.action_post()
|
||||
for line in vendor_bill.invoice_line_ids:
|
||||
pol = purchase.order_line.filtered(lambda l: l.product_id == line.product_id)
|
||||
self.assertTrue(pol)
|
||||
self.assertEqual(line.quantity, pol.product_qty)
|
||||
if line.product_id.type == 'service':
|
||||
self.assertNotEqual(line.date, line.date_maturity)
|
||||
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.
|
||||
adjust_quant = self.env['stock.quant'].with_context(inventory_mode=True).create({
|
||||
'product_id': self.product.id,
|
||||
'inventory_quantity': 20.0,
|
||||
'location_id': self.env.ref('stock.warehouse0').lot_stock_id.id,
|
||||
})
|
||||
adjust_quant.action_apply_inventory()
|
||||
self.assertEqual(self.product.virtual_available, 20.0)
|
||||
|
||||
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)
|
||||
for move_line in sale.picking_ids.mapped('move_lines.move_line_ids'):
|
||||
move_line.qty_done = move_line.product_uom_qty
|
||||
sale.picking_ids.button_validate()
|
||||
self.assertEqual(sale.picking_ids.state, 'done')
|
||||
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)
|
||||
for move_line in return_picking.mapped('move_lines.move_line_ids'):
|
||||
move_line.qty_done = move_line.product_uom_qty
|
||||
return_picking.button_validate()
|
||||
|
||||
self.assertTrue(all(l.qty_delivered == 0.0 for l in sale.order_line))
|
||||
69
product_cores/views/product_views.xml
Normal file
69
product_cores/views/product_views.xml
Normal 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', '>', 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>
|
||||
18
product_cores/views/purchase_views.xml
Normal file
18
product_cores/views/purchase_views.xml
Normal 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>
|
||||
21
product_cores/views/sale_views.xml
Normal file
21
product_cores/views/sale_views.xml
Normal 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>
|
||||
Reference in New Issue
Block a user