From 7282fd65cbe034c553a7a050a36dc53d52862e2d Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 17 Sep 2018 17:25:41 -0700 Subject: [PATCH 01/11] Initial commit of `delivery_hibou` and associated refactoring to `sale_planner` and `delivery_stamps` for 11.0 --- delivery_hibou/__init__.py | 1 + delivery_hibou/__manifest__.py | 24 +++ delivery_hibou/models/__init__.py | 2 + delivery_hibou/models/delivery.py | 152 +++++++++++++++++++ delivery_hibou/models/stock.py | 50 ++++++ delivery_hibou/tests/__init__.py | 1 + delivery_hibou/tests/test_delivery_hibou.py | 160 ++++++++++++++++++++ delivery_hibou/views/delivery_views.xml | 14 ++ delivery_hibou/views/stock_views.xml | 16 ++ 9 files changed, 420 insertions(+) create mode 100644 delivery_hibou/__init__.py create mode 100644 delivery_hibou/__manifest__.py create mode 100644 delivery_hibou/models/__init__.py create mode 100644 delivery_hibou/models/delivery.py create mode 100644 delivery_hibou/models/stock.py create mode 100644 delivery_hibou/tests/__init__.py create mode 100644 delivery_hibou/tests/test_delivery_hibou.py create mode 100644 delivery_hibou/views/delivery_views.xml create mode 100644 delivery_hibou/views/stock_views.xml diff --git a/delivery_hibou/__init__.py b/delivery_hibou/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/delivery_hibou/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py new file mode 100644 index 00000000..e53e01b9 --- /dev/null +++ b/delivery_hibou/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': 'Delivery Hibou', + 'summary': 'Adds underlying pinnings for things like "RMA Return Labels"', + 'version': '11.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Stock', + 'license': 'AGPL-3', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +This is a collection of "typical" carrier needs, and a bridge into Hibou modules like `delivery_partner` and `sale_planner`. +""", + 'depends': [ + 'delivery', + 'delivery_partner', + ], + 'demo': [], + 'data': [ + 'views/delivery_views.xml', + 'views/stock_views.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/delivery_hibou/models/__init__.py b/delivery_hibou/models/__init__.py new file mode 100644 index 00000000..29ce1386 --- /dev/null +++ b/delivery_hibou/models/__init__.py @@ -0,0 +1,2 @@ +from . import delivery +from . import stock diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py new file mode 100644 index 00000000..d8b6d41b --- /dev/null +++ b/delivery_hibou/models/delivery.py @@ -0,0 +1,152 @@ +from odoo import fields, models +from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES + + +class DeliveryCarrier(models.Model): + _inherit = 'delivery.carrier' + + automatic_insurance_value = fields.Float(string='Automatic Insurance Value', + help='Will be used during shipping to determine if the ' + 'picking\'s value warrants insurance being added.') + procurement_priority = fields.Selection(PROCUREMENT_PRIORITIES, + string='Procurement Priority', + help='Priority for this carrier. Will affect pickings ' + 'and procurements related to this carrier.') + + def get_insurance_value(self, order=None, picking=None): + value = 0.0 + if order: + if order.order_line: + value = sum(order.order_line.filtered(lambda l: l.type != 'service').mapped('price_subtotal')) + else: + return value + if picking: + value = picking.declared_value() + if picking.require_insurance == 'no': + value = 0.0 + elif picking.require_insurance == 'auto' and self.automatic_insurance_value and self.automatic_insurance_value > value: + value = 0.0 + return value + + def get_third_party_account(self, order=None, picking=None): + if order and order.shipping_account_id: + return order.shipping_account_id + if picking and picking.shipping_account_id: + return picking.shipping_account_id + return None + + def get_order_name(self, order=None, picking=None): + if order: + return order.name + if picking: + if picking.sale_id: + return picking.sale_id.name # + ' - ' + picking.name + return picking.name + return '' + + def get_attn(self, order=None, picking=None): + if order: + return order.client_order_ref + if picking and picking.sale_id: + return picking.sale_id.client_order_ref + # If Picking has a reference, decide what it is. + return False + + def _classify_picking(self, picking): + if picking.picking_type_id.code == 'incoming' and picking.location_id.usage == 'supplier' and picking.location_dest_id.usage == 'customer': + return 'dropship' + elif picking.picking_type_id.code == 'incoming' and picking.location_id.usage == 'customer' and picking.location_dest_id.usage == 'supplier': + return 'dropship_in' + elif picking.picking_type_id.code == 'incoming': + return 'in' + return 'out' + + # Shipper Company + + def get_shipper_company(self, order=None, picking=None): + """ + Shipper Company: The `res.partner` that provides the name of where the shipment is coming from. + """ + if order: + return order.company_id.partner_id + if picking: + return getattr(self, ('_get_shipper_company_%s' % (self._classify_picking(picking),)), + self._get_shipper_company_out)(picking) + return None + + def _get_shipper_company_dropship(self, picking): + return picking.company_id.partner_id + + def _get_shipper_company_dropship_in(self, picking): + return picking.company_id.partner_id + + def _get_shipper_company_in(self, picking): + return picking.company_id.partner_id + + def _get_shipper_company_out(self, picking): + return picking.company_id.partner_id + + # Shipper Warehouse + + def get_shipper_warehouse(self, order=None, picking=None): + """ + Shipper Warehouse: The `res.partner` that is basically the physical address a shipment is coming from. + """ + if order: + return order.warehouse_id.partner_id + if picking: + return getattr(self, ('_get_shipper_warehouse_%s' % (self._classify_picking(picking),)), + self._get_shipper_warehouse_out)(picking) + return None + + def _get_shipper_warehouse_dropship(self, picking): + return picking.partner_id + + def _get_shipper_warehouse_dropship_in(self, picking): + if picking.sale_id: + picking.sale_id.partner_shipping_id + return self._get_shipper_warehouse_dropship_in_no_sale(picking) + + def _get_shipper_warehouse_dropship_in_no_sale(self, picking): + return picking.company_id.partner_id + + def _get_shipper_warehouse_in(self, picking): + return picking.partner_id + + def _get_shipper_warehouse_out(self, picking): + return picking.picking_type_id.warehouse_id.partner_id + + # Recipient + + def get_recipient(self, order=None, picking=None): + """ + Recipient: The `res.partner` receiving the shipment. + """ + if order: + return order.partner_shipping_id + if picking: + return getattr(self, ('_get_recipient_%s' % (self._classify_picking(picking),)), + self._get_recipient_out)(picking) + return None + + def _get_recipient_dropship(self, picking): + if picking.sale_id: + return picking.sale_id.partner_shipping_id + return picking.sale_id.partner_shipping_id + + def _get_recipient_dropship_no_sale(self, picking): + return picking.company_id.partner_id + + def _get_recipient_dropship_in(self, picking): + return picking.picking_type_id.warehouse_id.partner_id + + def _get_recipient_in(self, picking): + return picking.picking_type_id.warehouse_id.partner_id + + def _get_recipient_out(self, picking): + return picking.partner_id + + + + + diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py new file mode 100644 index 00000000..c1a6a792 --- /dev/null +++ b/delivery_hibou/models/stock.py @@ -0,0 +1,50 @@ +from odoo import api, fields, models + + +class StockPicking(models.Model): + _inherit = 'stock.picking' + + shipping_account_id = fields.Many2one('partner.shipping.account', string='Shipping Account') + require_insurance = fields.Selection([ + ('auto', 'Automatic'), + ('yes', 'Yes'), + ('no', 'No'), + ], string='Require Insurance', default='auto', + help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.') + + @api.one + @api.depends('move_lines.priority', 'carrier_id') + def _compute_priority(self): + if self.carrier_id.procurement_priority: + self.priority = self.carrier_id.procurement_priority + else: + super(StockPicking, self)._compute_priority() + + @api.model + def create(self, values): + origin = values.get('origin') + if origin and not values.get('shipping_account_id'): + so = self.env['sale.order'].search([('name', '=', str(origin))], limit=1) + if so and so.shipping_account_id: + values['shipping_account_id'] = so.shipping_account_id.id + + res = super(StockPicking, self).create(values) + return res + + def declared_value(self): + self.ensure_one() + cost = sum([(l.product_id.standard_price * l.qty_done) for l in self.move_line_ids] or [0.0]) + if not cost: + # Assume Full Value + cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0]) + return cost + + + +class StockMove(models.Model): + _inherit = 'stock.move' + + def _prepare_procurement_values(self): + res = super(StockMove, self)._prepare_procurement_values() + res['priority'] = self.picking_id.priority or self.priority + return res diff --git a/delivery_hibou/tests/__init__.py b/delivery_hibou/tests/__init__.py new file mode 100644 index 00000000..d4e6373e --- /dev/null +++ b/delivery_hibou/tests/__init__.py @@ -0,0 +1 @@ +from . import test_delivery_hibou diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py new file mode 100644 index 00000000..68866f45 --- /dev/null +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -0,0 +1,160 @@ +from odoo.tests import common + + +class TestDeliveryHibou(common.TransactionCase): + + def setUp(self): + super(TestDeliveryHibou, self).setUp() + self.partner = self.env.ref('base.res_partner_address_13') + self.product = self.env.ref('product.product_product_7') + # Create Shipping Account + self.shipping_account = self.env['partner.shipping.account'].create({ + 'name': '123123', + 'delivery_type': 'other', + }) + # Create Carrier + self.delivery_product = self.env['product.product'].create({ + 'name': 'Test Carrier1 Delivery', + 'type': 'service', + }) + self.carrier = self.env['delivery.carrier'].create({ + 'name': 'Test Carrier1', + 'product_id': self.delivery_product.id, + }) + + def test_delivery_hibou(self): + # Assign a new shipping account + self.partner.shipping_account_id = self.shipping_account + + # Assign values to new Carrier + test_insurance_value = 600 + test_procurement_priority = '2' + self.carrier.automatic_insurance_value = test_insurance_value + self.carrier.procurement_priority = test_procurement_priority + + + sale_order = self.env['sale.order'].create({ + 'partner_id': self.partner.id, + 'partner_shipping_id': self.partner.id, + 'partner_invoice_id': self.partner.id, + 'carrier_id': self.carrier.id, + 'shipping_account_id': self.shipping_account.id, + 'order_line': [(0, 0, { + 'product_id': self.product.id, + })] + }) + sale_order.get_delivery_price() + sale_order.set_delivery_line() + sale_order.action_confirm() + # Make sure 3rd party Shipping Account is set. + self.assertEqual(sale_order.shipping_account_id, self.shipping_account) + + self.assertTrue(sale_order.picking_ids) + # Priority coming from Carrier procurement_priority + self.assertEqual(sale_order.picking_ids.priority, test_procurement_priority) + # 3rd party Shipping Account copied from Sale Order + self.assertEqual(sale_order.picking_ids.shipping_account_id, self.shipping_account) + self.assertEqual(sale_order.carrier_id.get_third_party_account(order=sale_order), self.shipping_account) + + # Test attn + test_ref = 'TEST100' + self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), False) + sale_order.client_order_ref = test_ref + self.assertEqual(sale_order.carrier_id.get_attn(order=sale_order), test_ref) + # The picking should get this ref as well + self.assertEqual(sale_order.picking_ids.carrier_id.get_attn(picking=sale_order.picking_ids), test_ref) + + # Test order_name + self.assertEqual(sale_order.carrier_id.get_order_name(order=sale_order), sale_order.name) + # The picking should get the same 'order_name' + self.assertEqual(sale_order.picking_ids.carrier_id.get_order_name(picking=sale_order.picking_ids), sale_order.name) + + def test_carrier_hibou_out(self): + test_insurance_value = 4000 + self.carrier.automatic_insurance_value = test_insurance_value + + picking_out = self.env.ref('stock.outgoing_shipment_main_warehouse') + self.assertEqual(picking_out.state, 'assigned') + + picking_out.carrier_id = self.carrier + + # This relies heavily on the 'stock' demo data. + # Should only have a single move_line_ids and it should not be done at all. + self.assertEqual(picking_out.move_line_ids.mapped('qty_done'), [0.0]) + self.assertEqual(picking_out.move_line_ids.mapped('product_uom_qty'), [15.0]) + self.assertEqual(picking_out.move_line_ids.mapped('product_id.standard_price'), [3300.0]) + + # The 'value' is assumed to be all of the product value from the initial demand. + self.assertEqual(picking_out.declared_value(), 15.0 * 3300.0) + self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), picking_out.declared_value()) + + # Workflow where user explicitly opts out of insurance on the picking level. + picking_out.require_insurance = 'no' + self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0) + picking_out.require_insurance = 'auto' + + # Lets choose to only delivery one piece at the moment. + # This does not meet the minimum on the carrier to have insurance value. + picking_out.move_line_ids.qty_done = 1.0 + self.assertEqual(picking_out.declared_value(), 3300.0) + self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 0.0) + # Workflow where user opts in to insurance. + picking_out.require_insurance = 'yes' + self.assertEqual(picking_out.carrier_id.get_insurance_value(picking=picking_out), 3300.0) + picking_out.require_insurance = 'auto' + + # Test with picking having 3rd party account. + self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), None) + picking_out.shipping_account_id = self.shipping_account + self.assertEqual(picking_out.carrier_id.get_third_party_account(picking=picking_out), self.shipping_account) + + # Shipment Time Methods! + self.assertEqual(picking_out.carrier_id._classify_picking(picking=picking_out), 'out') + self.assertEqual(picking_out.carrier_id.get_shipper_company(picking=picking_out), + picking_out.company_id.partner_id) + self.assertEqual(picking_out.carrier_id.get_shipper_warehouse(picking=picking_out), + picking_out.picking_type_id.warehouse_id.partner_id) + self.assertEqual(picking_out.carrier_id.get_recipient(picking=picking_out), + picking_out.partner_id) + # This picking has no `sale_id` + # Right now ATTN requires a sale_id, which this picking doesn't have (none of the stock ones do) + self.assertEqual(picking_out.carrier_id.get_attn(picking=picking_out), False) + self.assertEqual(picking_out.carrier_id.get_order_name(picking=picking_out), picking_out.name) + + def test_carrier_hibou_in(self): + picking_in = self.env.ref('stock.incomming_shipment1') + self.assertEqual(picking_in.state, 'assigned') + + picking_in.carrier_id = self.carrier + # This relies heavily on the 'stock' demo data. + # Should only have a single move_line_ids and it should not be done at all. + self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0]) + self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0]) + self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0]) + + self.assertEqual(picking_in.carrier_id._classify_picking(picking=picking_in), 'in') + self.assertEqual(picking_in.carrier_id.get_shipper_company(picking=picking_in), + picking_in.company_id.partner_id) + self.assertEqual(picking_in.carrier_id.get_shipper_warehouse(picking=picking_in), + picking_in.partner_id) + self.assertEqual(picking_in.carrier_id.get_recipient(picking=picking_in), + picking_in.picking_type_id.warehouse_id.partner_id) + + + + + + + + + + + + + + + + + + + diff --git a/delivery_hibou/views/delivery_views.xml b/delivery_hibou/views/delivery_views.xml new file mode 100644 index 00000000..01208bbd --- /dev/null +++ b/delivery_hibou/views/delivery_views.xml @@ -0,0 +1,14 @@ + + + + hibou.delivery.carrier.form + delivery.carrier + + + + + + + + + \ No newline at end of file diff --git a/delivery_hibou/views/stock_views.xml b/delivery_hibou/views/stock_views.xml new file mode 100644 index 00000000..78067b01 --- /dev/null +++ b/delivery_hibou/views/stock_views.xml @@ -0,0 +1,16 @@ + + + + + hibou.delivery.stock.picking_withcarrier.form.view + stock.picking + + + + + + + + + + From e16ba053d608c0846482808731726e82c9a83362 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 18 Feb 2019 09:10:12 -0800 Subject: [PATCH 02/11] MIG `delivery_hibou` to 12.0 --- delivery_hibou/tests/test_delivery_hibou.py | 1 + 1 file changed, 1 insertion(+) diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py index 68866f45..da8c4de4 100644 --- a/delivery_hibou/tests/test_delivery_hibou.py +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -74,6 +74,7 @@ class TestDeliveryHibou(common.TransactionCase): self.carrier.automatic_insurance_value = test_insurance_value picking_out = self.env.ref('stock.outgoing_shipment_main_warehouse') + picking_out.action_assign() self.assertEqual(picking_out.state, 'assigned') picking_out.carrier_id = self.carrier From f105ace67a9f9a51ec2c438dee9fe967b5b18cfa Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 16 Mar 2019 14:11:35 -0700 Subject: [PATCH 03/11] FIX `delivery_hibou` Fallback during dropship. --- delivery_hibou/models/delivery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index d8b6d41b..c7b8ed2c 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -132,7 +132,7 @@ class DeliveryCarrier(models.Model): def _get_recipient_dropship(self, picking): if picking.sale_id: return picking.sale_id.partner_shipping_id - return picking.sale_id.partner_shipping_id + return picking.partner_id def _get_recipient_dropship_no_sale(self, picking): return picking.company_id.partner_id From c7dcca056c0922150ef0f01af97253cbe1f10ffe Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Thu, 8 Aug 2019 09:37:57 -0700 Subject: [PATCH 04/11] IMP `delivery_hibou` Add util method `is_amazon` to override with specific implementation --- delivery_hibou/models/delivery.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index c7b8ed2c..03c30aea 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -13,6 +13,8 @@ class DeliveryCarrier(models.Model): help='Priority for this carrier. Will affect pickings ' 'and procurements related to this carrier.') + # Utility + def get_insurance_value(self, order=None, picking=None): value = 0.0 if order: @@ -61,6 +63,16 @@ class DeliveryCarrier(models.Model): return 'in' return 'out' + def is_amazon(self, order=None, picking=None): + """ + Amazon MWS orders potentially need to be flagged for + clean up on the carrier's side. + + Override to return based on criteria in your company. + :return: + """ + return False + # Shipper Company def get_shipper_company(self, order=None, picking=None): @@ -145,8 +157,3 @@ class DeliveryCarrier(models.Model): def _get_recipient_out(self, picking): return picking.partner_id - - - - - From 063d776df4f8d9348791507774b48142e07fc446 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 3 Jul 2020 15:53:22 -0700 Subject: [PATCH 05/11] [MIG] delivery_hibou: to Odoo 13.0 --- delivery_hibou/__manifest__.py | 2 +- delivery_hibou/models/stock.py | 2 -- delivery_hibou/tests/test_delivery_hibou.py | 31 ++++++--------------- 3 files changed, 10 insertions(+), 25 deletions(-) diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index e53e01b9..4897f03c 100644 --- a/delivery_hibou/__manifest__.py +++ b/delivery_hibou/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Delivery Hibou', 'summary': 'Adds underlying pinnings for things like "RMA Return Labels"', - 'version': '11.0.1.0.0', + 'version': '13.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Stock', 'license': 'AGPL-3', diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index c1a6a792..68a48e83 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -12,7 +12,6 @@ class StockPicking(models.Model): ], string='Require Insurance', default='auto', help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.') - @api.one @api.depends('move_lines.priority', 'carrier_id') def _compute_priority(self): if self.carrier_id.procurement_priority: @@ -40,7 +39,6 @@ class StockPicking(models.Model): return cost - class StockMove(models.Model): _inherit = 'stock.move' diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py index da8c4de4..7b47b750 100644 --- a/delivery_hibou/tests/test_delivery_hibou.py +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -37,14 +37,20 @@ class TestDeliveryHibou(common.TransactionCase): 'partner_id': self.partner.id, 'partner_shipping_id': self.partner.id, 'partner_invoice_id': self.partner.id, - 'carrier_id': self.carrier.id, 'shipping_account_id': self.shipping_account.id, 'order_line': [(0, 0, { 'product_id': self.product.id, })] }) - sale_order.get_delivery_price() - sale_order.set_delivery_line() + self.assertFalse(sale_order.carrier_id) + action = sale_order.action_open_delivery_wizard() + form = common.Form(self.env[action['res_model']].with_context(**action.get('context', {}))) + form.carrier_id = self.carrier + wizard = form.save() + wizard.button_confirm() + + #sale_order.set_delivery_line() + self.assertEqual(sale_order.carrier_id, self.carrier) sale_order.action_confirm() # Make sure 3rd party Shipping Account is set. self.assertEqual(sale_order.shipping_account_id, self.shipping_account) @@ -140,22 +146,3 @@ class TestDeliveryHibou(common.TransactionCase): picking_in.partner_id) self.assertEqual(picking_in.carrier_id.get_recipient(picking=picking_in), picking_in.picking_type_id.warehouse_id.partner_id) - - - - - - - - - - - - - - - - - - - From caea9ec0078e6524dd6f889827fa31f3ea988c66 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 14 Oct 2020 08:18:50 -0700 Subject: [PATCH 06/11] [FIX] delivery_hibou: correct picking priority when computed on more than one picking --- delivery_hibou/models/stock.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index 68a48e83..35559223 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -14,10 +14,10 @@ class StockPicking(models.Model): @api.depends('move_lines.priority', 'carrier_id') def _compute_priority(self): - if self.carrier_id.procurement_priority: - self.priority = self.carrier_id.procurement_priority - else: - super(StockPicking, self)._compute_priority() + with_carrier_priority = self.filtered(lambda p: p.carrier_id.procurement_priority) + for picking in with_carrier_priority: + picking.priority = picking.carrier_id.procurement_priority + super(StockPicking, (self-with_carrier_priority))._compute_priority() @api.model def create(self, values): From e54b35d9f610bb5ef9c873f744653ba79516dbab Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Tue, 17 Nov 2020 10:48:10 -0800 Subject: [PATCH 07/11] [MIG] delivery_hibou: to 14.0 --- delivery_hibou/__manifest__.py | 2 +- delivery_hibou/models/stock.py | 25 +++++++++------------ delivery_hibou/tests/test_delivery_hibou.py | 10 ++++----- 3 files changed, 16 insertions(+), 21 deletions(-) diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index 4897f03c..5b7350e7 100644 --- a/delivery_hibou/__manifest__.py +++ b/delivery_hibou/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Delivery Hibou', 'summary': 'Adds underlying pinnings for things like "RMA Return Labels"', - 'version': '13.0.1.0.0', + 'version': '14.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Stock', 'license': 'AGPL-3', diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index 35559223..725a7bbc 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -12,12 +12,11 @@ class StockPicking(models.Model): ], string='Require Insurance', default='auto', help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.') - @api.depends('move_lines.priority', 'carrier_id') - def _compute_priority(self): - with_carrier_priority = self.filtered(lambda p: p.carrier_id.procurement_priority) - for picking in with_carrier_priority: - picking.priority = picking.carrier_id.procurement_priority - super(StockPicking, (self-with_carrier_priority))._compute_priority() + @api.onchange('carrier_id') + def _onchange_carrier_id_for_priority(self): + for picking in self: + if picking.carrier_id and picking.carrier_id.procurement_priority: + picking.priority = picking.carrier_id.procurement_priority @api.model def create(self, values): @@ -26,6 +25,11 @@ class StockPicking(models.Model): so = self.env['sale.order'].search([('name', '=', str(origin))], limit=1) if so and so.shipping_account_id: values['shipping_account_id'] = so.shipping_account_id.id + carrier_id = values.get('carrier_id') + if carrier_id: + carrier = self.env['delivery.carrier'].browse(carrier_id) + if carrier.procurement_priority: + values['priority'] = carrier.procurement_priority res = super(StockPicking, self).create(values) return res @@ -37,12 +41,3 @@ class StockPicking(models.Model): # Assume Full Value cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0]) return cost - - -class StockMove(models.Model): - _inherit = 'stock.move' - - def _prepare_procurement_values(self): - res = super(StockMove, self)._prepare_procurement_values() - res['priority'] = self.picking_id.priority or self.priority - return res diff --git a/delivery_hibou/tests/test_delivery_hibou.py b/delivery_hibou/tests/test_delivery_hibou.py index 7b47b750..2fc53309 100644 --- a/delivery_hibou/tests/test_delivery_hibou.py +++ b/delivery_hibou/tests/test_delivery_hibou.py @@ -24,11 +24,11 @@ class TestDeliveryHibou(common.TransactionCase): def test_delivery_hibou(self): # Assign a new shipping account - self.partner.shipping_account_id = self.shipping_account + self.partner.shipping_account_ids = self.shipping_account # Assign values to new Carrier test_insurance_value = 600 - test_procurement_priority = '2' + test_procurement_priority = '1' self.carrier.automatic_insurance_value = test_insurance_value self.carrier.procurement_priority = test_procurement_priority @@ -135,9 +135,9 @@ class TestDeliveryHibou(common.TransactionCase): picking_in.carrier_id = self.carrier # This relies heavily on the 'stock' demo data. # Should only have a single move_line_ids and it should not be done at all. - self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0]) - self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0]) - self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0]) + self.assertEqual(picking_in.move_line_ids.mapped('qty_done'), [0.0, 0.0, 0.0]) + self.assertEqual(picking_in.move_line_ids.mapped('product_uom_qty'), [35.0, 10.0, 12.0]) + self.assertEqual(picking_in.move_line_ids.mapped('product_id.standard_price'), [55.0, 35.0, 1700.0]) self.assertEqual(picking_in.carrier_id._classify_picking(picking=picking_in), 'in') self.assertEqual(picking_in.carrier_id.get_shipper_company(picking=picking_in), From 514bdaab11468a02a1415f54565754c3e81f4209 Mon Sep 17 00:00:00 2001 From: Cedric Collins Date: Mon, 13 Sep 2021 18:04:54 -0500 Subject: [PATCH 08/11] [IMP] delivery_hibou: add rate_shipment_multi --- delivery_hibou/models/delivery.py | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 03c30aea..4b478dd8 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -157,3 +157,45 @@ class DeliveryCarrier(models.Model): def _get_recipient_out(self, picking): return picking.partner_id + + # -------------------------- # + # API for external providers # + # -------------------------- # + def rate_shipment_multi(self, order=None, picking=None): + ''' Compute the price of the order shipment + + :param order: record of sale.order or None + :param picking: record of stock.picking or None + :return list: dict: { + 'carrier': delivery.carrier(), + 'success': boolean, + 'price': a float, + 'error_message': a string containing an error message, + 'warning_message': a string containing a warning message, + 'date_planned': a datetime for when the shipment is supposed to leave, + 'date_delivered': a datetime for when the shipment is supposed to arrive, + 'transit_days': a Float for how many days it takes in transit, + 'service_code': a string that represents the service level/agreement, + } + + e.g. self == delivery.carrier(5, 6) + then return might be: + [ + {'carrier': delivery.carrier(5), 'price': 10.50, 'service_code': 'GROUND_HOME_DELIVERY', ...}, + {'carrier': delivery.carrier(7), 'price': 12.99, 'service_code': 'FEDEX_EXPRESS_SAVER', ...}, # NEW! + {'carrier': delivery.carrier(6), 'price': 8.0, 'service_code': 'USPS_PRI', ...}, + ] + ''' + self.ensure_one() + + if picking: + self = self.with_context(date_planned=fields.Datetime.now()) + else: + self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now())) + + res = [] + for carrier in self: + if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type): + carrier_rates = getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, picking=picking) + res += carrier_rates + return res From 5537f8a2113ca874c855729d980c7944f682d351 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 20 Sep 2021 17:41:42 -0700 Subject: [PATCH 09/11] [IMP] delivery_hibou: provide mechanisms for per-package carrier and tracking --- delivery_hibou/__manifest__.py | 2 +- delivery_hibou/models/delivery.py | 50 +++++++++- delivery_hibou/models/stock.py | 118 +++++++++++++++++++++++- delivery_hibou/views/delivery_views.xml | 12 +++ delivery_hibou/views/stock_views.xml | 32 +++++++ 5 files changed, 209 insertions(+), 5 deletions(-) diff --git a/delivery_hibou/__manifest__.py b/delivery_hibou/__manifest__.py index 5b7350e7..de31aeea 100644 --- a/delivery_hibou/__manifest__.py +++ b/delivery_hibou/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Delivery Hibou', 'summary': 'Adds underlying pinnings for things like "RMA Return Labels"', - 'version': '14.0.1.0.0', + 'version': '14.0.1.1.0', 'author': "Hibou Corp.", 'category': 'Stock', 'license': 'AGPL-3', diff --git a/delivery_hibou/models/delivery.py b/delivery_hibou/models/delivery.py index 4b478dd8..d281fb5f 100644 --- a/delivery_hibou/models/delivery.py +++ b/delivery_hibou/models/delivery.py @@ -1,5 +1,6 @@ from odoo import fields, models from odoo.addons.stock.models.stock_move import PROCUREMENT_PRIORITIES +from odoo.exceptions import UserError class DeliveryCarrier(models.Model): @@ -161,11 +162,12 @@ class DeliveryCarrier(models.Model): # -------------------------- # # API for external providers # # -------------------------- # - def rate_shipment_multi(self, order=None, picking=None): + def rate_shipment_multi(self, order=None, picking=None, packages=None): ''' Compute the price of the order shipment :param order: record of sale.order or None :param picking: record of stock.picking or None + :param packages: recordset of stock.quant.package or None (requires picking also set) :return list: dict: { 'carrier': delivery.carrier(), 'success': boolean, @@ -176,6 +178,7 @@ class DeliveryCarrier(models.Model): 'date_delivered': a datetime for when the shipment is supposed to arrive, 'transit_days': a Float for how many days it takes in transit, 'service_code': a string that represents the service level/agreement, + 'package': stock.quant.package(), } e.g. self == delivery.carrier(5, 6) @@ -190,12 +193,53 @@ class DeliveryCarrier(models.Model): if picking: self = self.with_context(date_planned=fields.Datetime.now()) + if not packages: + packages = picking.package_ids else: + if packages: + raise UserError('Cannot rate package without picking.') self = self.with_context(date_planned=(order.date_planned or fields.Datetime.now())) res = [] for carrier in self: + carrier_packages = packages.filtered(lambda p: not p.carrier_tracking_ref and + (not p.carrier_id or p.carrier_id == carrier) and + p.packaging_id.package_carrier_type in (False, '', 'none', carrier.delivery_type)) + if packages and not carrier_packages: + continue if hasattr(carrier, '%s_rate_shipment_multi' % self.delivery_type): - carrier_rates = getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, picking=picking) - res += carrier_rates + try: + res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, + picking=picking, + packages=carrier_packages) + except TypeError: + # TODO remove catch if after Odoo 14 + # This is intended to find ones that don't support packages= kwarg + res += getattr(carrier, '%s_rate_shipment_multi' % carrier.delivery_type)(order=order, + picking=picking) + return res + + def cancel_shipment(self, pickings, packages=None): + ''' Cancel a shipment + + :param pickings: A recordset of pickings + :param packages: Optional recordset of packages (should be for this carrier) + ''' + self.ensure_one() + if hasattr(self, '%s_cancel_shipment' % self.delivery_type): + # No good way to tell if this method takes the kwarg for packages + if packages: + try: + return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings, packages=packages) + except TypeError: + # we won't be able to cancel the packages properly + # here we will TRY to make a good call here where we put the package references into the picking + # and let the original mechanisms try to work here + tracking_ref = ','.join(packages.mapped('carrier_tracking_ref')) + pickings.write({ + 'carrier_id': self.id, + 'carrier_tracking_ref': tracking_ref, + }) + + return getattr(self, '%s_cancel_shipment' % self.delivery_type)(pickings) diff --git a/delivery_hibou/models/stock.py b/delivery_hibou/models/stock.py index 725a7bbc..ec2d2664 100644 --- a/delivery_hibou/models/stock.py +++ b/delivery_hibou/models/stock.py @@ -1,4 +1,27 @@ -from odoo import api, fields, models +from odoo import api, fields, models, _ +from odoo.exceptions import UserError + + +class StockQuantPackage(models.Model): + _inherit = 'stock.quant.package' + + carrier_id = fields.Many2one('delivery.carrier', string='Carrier') + carrier_tracking_ref = fields.Char(string='Tracking Reference') + + def _get_active_picking(self): + picking_id = self._context.get('active_id') + picking_model = self._context.get('active_model') + if not picking_id or picking_model != 'stock.picking': + raise UserError('Cannot cancel package other than through shipment/picking.') + return self.env['stock.picking'].browse(picking_id) + + def send_to_shipper(self): + picking = self._get_active_picking() + picking.send_to_shipper(packages=self) + + def cancel_shipment(self): + picking = self._get_active_picking() + picking.cancel_shipment(packages=self) class StockPicking(models.Model): @@ -11,6 +34,16 @@ class StockPicking(models.Model): ('no', 'No'), ], string='Require Insurance', default='auto', help='If your carrier supports it, auto should be calculated off of the "Automatic Insurance Value" field.') + package_carrier_tracking_ref = fields.Char(string='Package Tracking Numbers', compute='_compute_package_carrier_tracking_ref') + + @api.depends('package_ids.carrier_tracking_ref') + def _compute_package_carrier_tracking_ref(self): + for picking in self: + package_refs = picking.package_ids.filtered('carrier_tracking_ref').mapped('carrier_tracking_ref') + if package_refs: + picking.package_carrier_tracking_ref = ','.join(package_refs) + else: + picking.package_carrier_tracking_ref = False @api.onchange('carrier_id') def _onchange_carrier_id_for_priority(self): @@ -41,3 +74,86 @@ class StockPicking(models.Model): # Assume Full Value cost = sum([(l.product_id.standard_price * l.product_uom_qty) for l in self.move_lines] or [0.0]) return cost + + def clear_carrier_tracking_ref(self): + self.write({'carrier_tracking_ref': False}) + + def reset_carrier_tracking_ref(self): + for picking in self: + picking.carrier_tracking_ref = picking.package_carrier_tracking_ref + + # Override to send to specific packaging carriers + def send_to_shipper(self, packages=None): + self.ensure_one() + if not packages: + packages = self.package_ids + package_carriers = packages.mapped('carrier_id') + if not package_carriers: + # Original behavior + return super().send_to_shipper() + + tracking_numbers = [] + carrier_prices = [] + order_currency = self.sale_id.currency_id or self.company_id.currency_id + for carrier in package_carriers: + self.carrier_id = carrier + carrier_packages = packages.filtered(lambda p: p.carrier_id == carrier) + res = carrier.send_shipping(self) + if res: + res = res[0] + if carrier.free_over and self.sale_id and self.sale_id._compute_amount_total_without_delivery() >= carrier.amount: + res['exact_price'] = 0.0 + carrier_price = res['exact_price'] * (1.0 + (self.carrier_id.margin / 100.0)) + carrier_prices.append(carrier_price) + tracking_number = '' + if res['tracking_number']: + tracking_number = res['tracking_number'] + tracking_numbers.append(tracking_number) + # Try to add tracking to the individual packages. + potential_tracking_numbers = tracking_number.split(',') + if len(potential_tracking_numbers) >= len(carrier_packages): + for t, p in zip(potential_tracking_numbers, carrier_packages): + p.carrier_tracking_ref = t + else: + carrier_packages.write({'carrier_tracking_ref': tracking_number}) + msg = _( + "Shipment sent to carrier %(carrier_name)s for shipping with tracking number %(ref)s
Cost: %(price).2f %(currency)s", + carrier_name=carrier.name, + ref=tracking_number, + price=carrier_price, + currency=order_currency.name + ) + self.message_post(body=msg) + + self.carrier_price = sum(carrier_prices or [0.0]) + self.carrier_tracking_ref = ','.join(tracking_numbers or ['']) + self._add_delivery_cost_to_so() + + # Override to provide per-package versions... + def cancel_shipment(self, packages=None): + pickings_with_package_tracking = self.filtered(lambda p: p.package_carrier_tracking_ref) + for picking in pickings_with_package_tracking: + if packages: + current_packages = packages + else: + current_packages = picking.package_ids + # Packages without a carrier can just be cleared + packages_without_carrier = current_packages.filtered(lambda p: not p.carrier_id and p.carrier_tracking_ref) + packages_without_carrier.write({ + 'carrier_tracking_ref': False, + }) + # Packages with carrier can use the carrier method + packages_with_carrier = current_packages.filtered(lambda p: p.carrier_id and p.carrier_tracking_ref) + carriers = packages_with_carrier.mapped('carrier_id') + for carrier in carriers: + carrier_packages = packages_with_carrier.filtered(lambda p: p.carrier_id == carrier) + carrier.cancel_shipment(self, packages=carrier_packages) + package_refs = ','.join(carrier_packages.mapped('carrier_tracking_ref')) + msg = "Shipment %s cancelled" % package_refs + picking.message_post(body=msg) + carrier_packages.write({'carrier_tracking_ref': False}) + + pickings_without_package_tracking = self - pickings_with_package_tracking + if pickings_without_package_tracking: + # use original on these + super(StockPicking, pickings_without_package_tracking).cancel_shipment() diff --git a/delivery_hibou/views/delivery_views.xml b/delivery_hibou/views/delivery_views.xml index 01208bbd..960df1c2 100644 --- a/delivery_hibou/views/delivery_views.xml +++ b/delivery_hibou/views/delivery_views.xml @@ -11,4 +11,16 @@ + + + hibou.choose.delivery.package.form + choose.delivery.package + + + + [('package_carrier_type', '!=', False)] + + + + \ No newline at end of file diff --git a/delivery_hibou/views/stock_views.xml b/delivery_hibou/views/stock_views.xml index 78067b01..6bb47f34 100644 --- a/delivery_hibou/views/stock_views.xml +++ b/delivery_hibou/views/stock_views.xml @@ -1,14 +1,46 @@ + + hibou.stock.quant.package.form + stock.quant.package + + + + + + + hibou.delivery.stock.picking_withcarrier.form.view stock.picking + + +