From d41acbf3b20ca89e28f427778fef663f1983bb8d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Jul 2020 09:12:28 -0700 Subject: [PATCH 1/4] [MOV] product_cores: from Hibou Suite Enterprise for 13.0 --- product_cores/__init__.py | 3 + product_cores/__manifest__.py | 25 +++ product_cores/models/__init__.py | 5 + product_cores/models/product.py | 61 ++++++ product_cores/models/purchase.py | 64 ++++++ product_cores/models/sale.py | 68 +++++++ product_cores/tests/__init__.py | 3 + product_cores/tests/test_product_cores.py | 236 ++++++++++++++++++++++ product_cores/views/product_views.xml | 69 +++++++ product_cores/views/purchase_views.xml | 18 ++ product_cores/views/sale_views.xml | 21 ++ 11 files changed, 573 insertions(+) create mode 100644 product_cores/__init__.py create mode 100755 product_cores/__manifest__.py create mode 100644 product_cores/models/__init__.py create mode 100644 product_cores/models/product.py create mode 100644 product_cores/models/purchase.py create mode 100644 product_cores/models/sale.py create mode 100644 product_cores/tests/__init__.py create mode 100644 product_cores/tests/test_product_cores.py create mode 100644 product_cores/views/product_views.xml create mode 100644 product_cores/views/purchase_views.xml create mode 100644 product_cores/views/sale_views.xml diff --git a/product_cores/__init__.py b/product_cores/__init__.py new file mode 100644 index 00000000..09434554 --- /dev/null +++ b/product_cores/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import models diff --git a/product_cores/__manifest__.py b/product_cores/__manifest__.py new file mode 100755 index 00000000..e18a9624 --- /dev/null +++ b/product_cores/__manifest__.py @@ -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. ', + '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, +} diff --git a/product_cores/models/__init__.py b/product_cores/models/__init__.py new file mode 100644 index 00000000..b29ef4f6 --- /dev/null +++ b/product_cores/models/__init__.py @@ -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 diff --git a/product_cores/models/product.py b/product_cores/models/product.py new file mode 100644 index 00000000..b88df8d9 --- /dev/null +++ b/product_cores/models/product.py @@ -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') diff --git a/product_cores/models/purchase.py b/product_cores/models/purchase.py new file mode 100644 index 00000000..8fef0a60 --- /dev/null +++ b/product_cores/models/purchase.py @@ -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() diff --git a/product_cores/models/sale.py b/product_cores/models/sale.py new file mode 100644 index 00000000..327af418 --- /dev/null +++ b/product_cores/models/sale.py @@ -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 diff --git a/product_cores/tests/__init__.py b/product_cores/tests/__init__.py new file mode 100644 index 00000000..c6970af4 --- /dev/null +++ b/product_cores/tests/__init__.py @@ -0,0 +1,3 @@ +# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details. + +from . import test_product_cores diff --git a/product_cores/tests/test_product_cores.py b/product_cores/tests/test_product_cores.py new file mode 100644 index 00000000..41edf769 --- /dev/null +++ b/product_cores/tests/test_product_cores.py @@ -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)) diff --git a/product_cores/views/product_views.xml b/product_cores/views/product_views.xml new file mode 100644 index 00000000..da7157d9 --- /dev/null +++ b/product_cores/views/product_views.xml @@ -0,0 +1,69 @@ + + + + + product.template.product.form.inherit + product.template + + + +
+ +
+
+ + + + + + + +
+
+ + + product.product.form.inherit + product.product + + + +
+ +
+
+ + + + + + + +
+
+ + + + product.supplierinfo.tree.view.inherit + product.supplierinfo + + + + + + + + + + product.supplierinfo.form.view.inherit + product.supplierinfo + + + + + + + + +
diff --git a/product_cores/views/purchase_views.xml b/product_cores/views/purchase_views.xml new file mode 100644 index 00000000..63b8aec4 --- /dev/null +++ b/product_cores/views/purchase_views.xml @@ -0,0 +1,18 @@ + + + + + purchase.order.form.inherit + purchase.order + + + + + + + + + + + + diff --git a/product_cores/views/sale_views.xml b/product_cores/views/sale_views.xml new file mode 100644 index 00000000..6b2b88f7 --- /dev/null +++ b/product_cores/views/sale_views.xml @@ -0,0 +1,21 @@ + + + + + sale.order.form.inherit + sale.order + + + + + + + + + + + + + + + From 2798b04643363c7f3c16fcbef3d5104371b4606d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 28 Oct 2020 15:17:21 -0700 Subject: [PATCH 2/4] [IMP] product_cores: ensure PO received qty is updated and utilize accounting date_maturity for expiration and reporting --- product_cores/__manifest__.py | 4 +-- product_cores/models/__init__.py | 1 + product_cores/models/account.py | 41 +++++++++++++++++++++++ product_cores/models/purchase.py | 8 ++++- product_cores/tests/test_product_cores.py | 16 ++++++++- 5 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 product_cores/models/account.py diff --git a/product_cores/__manifest__.py b/product_cores/__manifest__.py index e18a9624..08b04f83 100755 --- a/product_cores/__manifest__.py +++ b/product_cores/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Product Cores', 'author': 'Hibou Corp. ', - 'version': '13.0.1.0.0', + 'version': '13.0.1.0.1', 'category': 'Tools', 'license': 'OPL-1', 'summary': 'Charge customers core deposits.', @@ -13,7 +13,7 @@ Charge customers core deposits. 'website': 'https://hibou.io/', 'depends': [ 'sale_stock', - 'purchase', + 'purchase_stock', ], 'data': [ 'views/product_views.xml', diff --git a/product_cores/models/__init__.py b/product_cores/models/__init__.py index b29ef4f6..db72a159 100644 --- a/product_cores/models/__init__.py +++ b/product_cores/models/__init__.py @@ -1,5 +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 diff --git a/product_cores/models/account.py b/product_cores/models/account.py new file mode 100644 index 00000000..c2bc3d61 --- /dev/null +++ b/product_cores/models/account.py @@ -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): + if self._context.get('move_reverse_cancel'): + return super(AccountMove, self).post() + self._product_core_set_date_maturity() + return super(AccountMove, self).post() + + 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.type in ('in_invoice', 'in_refund', 'in_receipt'): + # derive from purchase + if 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.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.type in ('out_invoice', 'out_refund', 'out_receipt'): + # derive from sale + if 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.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 diff --git a/product_cores/models/purchase.py b/product_cores/models/purchase.py index 8fef0a60..3f9673ae 100644 --- a/product_cores/models/purchase.py +++ b/product_cores/models/purchase.py @@ -34,7 +34,7 @@ class PurchaseOrderLine(models.Model): 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: + 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')\ @@ -62,3 +62,9 @@ class PurchaseOrderLine(models.Model): }) 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 diff --git a/product_cores/tests/test_product_cores.py b/product_cores/tests/test_product_cores.py index 41edf769..b2f2181e 100644 --- a/product_cores/tests/test_product_cores.py +++ b/product_cores/tests/test_product_cores.py @@ -43,7 +43,8 @@ class TestProductCores(common.TransactionCase): '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])] + 'taxes_id': [(6, 0, [self.sale_tax_service.id])], + 'product_core_validity': 30, }) self.product_core = self.env['product.product'].create({ 'name': 'Turbo Core', @@ -109,7 +110,14 @@ class TestProductCores(common.TransactionCase): 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')) @@ -118,6 +126,12 @@ class TestProductCores(common.TransactionCase): vendor_bill = f.save() self.assertEqual(len(vendor_bill.invoice_line_ids), 2) vendor_bill.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 From bf5318710ec1296278a9296351caeca5c356601e Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Dec 2021 10:24:38 -0800 Subject: [PATCH 3/4] [MIG] product_cores: to Odoo 14.0 --- product_cores/__manifest__.py | 2 +- product_cores/models/account.py | 18 +++++++++--------- product_cores/tests/test_product_cores.py | 15 ++++++++------- 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/product_cores/__manifest__.py b/product_cores/__manifest__.py index 08b04f83..d245ddf1 100755 --- a/product_cores/__manifest__.py +++ b/product_cores/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Product Cores', 'author': 'Hibou Corp. ', - 'version': '13.0.1.0.1', + 'version': '14.0.1.0.0', 'category': 'Tools', 'license': 'OPL-1', 'summary': 'Charge customers core deposits.', diff --git a/product_cores/models/account.py b/product_cores/models/account.py index c2bc3d61..7ebf1401 100644 --- a/product_cores/models/account.py +++ b/product_cores/models/account.py @@ -7,32 +7,32 @@ from odoo import models class AccountMove(models.Model): _inherit = 'account.move' - def post(self): + def _post(self, soft=True): if self._context.get('move_reverse_cancel'): - return super(AccountMove, self).post() + return super(AccountMove, self)._post(soft) self._product_core_set_date_maturity() - return super(AccountMove, self).post() + 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.type in ('in_invoice', 'in_refund', 'in_receipt'): + if move.move_type in ('in_invoice', 'in_refund', 'in_receipt'): # derive from purchase - if move.type == 'in_refund' and line.purchase_line_id: + 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.type == 'in_invoice') + 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.type in ('out_invoice', 'out_refund', 'out_receipt'): + elif move.move_type in ('out_invoice', 'out_refund', 'out_receipt'): # derive from sale - if 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.type == 'out_invoice') + 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: diff --git a/product_cores/tests/test_product_cores.py b/product_cores/tests/test_product_cores.py index b2f2181e..e7a760e8 100644 --- a/product_cores/tests/test_product_cores.py +++ b/product_cores/tests/test_product_cores.py @@ -125,7 +125,7 @@ class TestProductCores(common.TransactionCase): f.purchase_id = purchase vendor_bill = f.save() self.assertEqual(len(vendor_bill.invoice_line_ids), 2) - vendor_bill.post() + 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) @@ -209,9 +209,10 @@ class TestProductCores(common.TransactionCase): 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() + 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. @@ -243,8 +244,8 @@ class TestProductCores(common.TransactionCase): 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() + 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)) From a4a9706059e32fc8e58b5ab4eb0100aeab3dcf91 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Dec 2021 11:51:59 -0800 Subject: [PATCH 4/4] [MIG] product_cores: to Odoo 15.0 --- product_cores/__manifest__.py | 2 +- product_cores/models/purchase.py | 1 + product_cores/models/sale.py | 1 + product_cores/tests/test_product_cores.py | 21 ++++++--------------- 4 files changed, 9 insertions(+), 16 deletions(-) diff --git a/product_cores/__manifest__.py b/product_cores/__manifest__.py index d245ddf1..83abf041 100755 --- a/product_cores/__manifest__.py +++ b/product_cores/__manifest__.py @@ -3,7 +3,7 @@ { 'name': 'Product Cores', 'author': 'Hibou Corp. ', - 'version': '14.0.1.0.0', + 'version': '15.0.1.0.0', 'category': 'Tools', 'license': 'OPL-1', 'summary': 'Charge customers core deposits.', diff --git a/product_cores/models/purchase.py b/product_cores/models/purchase.py index 3f9673ae..95ba7087 100644 --- a/product_cores/models/purchase.py +++ b/product_cores/models/purchase.py @@ -17,6 +17,7 @@ class PurchaseOrder(models.Model): 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 diff --git a/product_cores/models/sale.py b/product_cores/models/sale.py index 327af418..c1cfb7d9 100644 --- a/product_cores/models/sale.py +++ b/product_cores/models/sale.py @@ -17,6 +17,7 @@ class SaleOrder(models.Model): 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 diff --git a/product_cores/tests/test_product_cores.py b/product_cores/tests/test_product_cores.py index e7a760e8..2bbe195c 100644 --- a/product_cores/tests/test_product_cores.py +++ b/product_cores/tests/test_product_cores.py @@ -153,22 +153,13 @@ class TestProductCores(common.TransactionCase): def test_02_sale(self): # Need Inventory. - adjustment = self.env['stock.inventory'].create({ - 'name': 'Initial', - 'product_ids': [(4, self.product.id)], + 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, }) - 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() + adjust_quant.action_apply_inventory() + self.assertEqual(self.product.virtual_available, 20.0) sale = self.env['sale.order'].create({ 'partner_id': self.customer.id,