From 09839639e296fcd2979ac0b8f6bde35a8c10a5ae Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 9 May 2018 09:17:09 -0700 Subject: [PATCH 01/44] Initial commit of `sale_sourced_by_line` for 11.0 --- sale_sourced_by_line/README.rst | 17 ++++++++++ sale_sourced_by_line/__init__.py | 1 + sale_sourced_by_line/__manifest__.py | 29 +++++++++++++++++ sale_sourced_by_line/models/__init__.py | 1 + sale_sourced_by_line/models/sale.py | 25 +++++++++++++++ sale_sourced_by_line/tests/__init__.py | 1 + .../tests/test_sale_sources.py | 23 +++++++++++++ sale_sourced_by_line/views/sale_views.xml | 32 +++++++++++++++++++ 8 files changed, 129 insertions(+) create mode 100644 sale_sourced_by_line/README.rst create mode 100644 sale_sourced_by_line/__init__.py create mode 100644 sale_sourced_by_line/__manifest__.py create mode 100644 sale_sourced_by_line/models/__init__.py create mode 100644 sale_sourced_by_line/models/sale.py create mode 100644 sale_sourced_by_line/tests/__init__.py create mode 100644 sale_sourced_by_line/tests/test_sale_sources.py create mode 100644 sale_sourced_by_line/views/sale_views.xml diff --git a/sale_sourced_by_line/README.rst b/sale_sourced_by_line/README.rst new file mode 100644 index 00000000..2c747686 --- /dev/null +++ b/sale_sourced_by_line/README.rst @@ -0,0 +1,17 @@ +============================ +Hibou - Sale Sourced by Line +============================ + +Adds warehouse and planned date fields to sale order lines. Will split the delivery orders +to every distinct warehouse. + +Additionally, adds fields per line and to the sale order to set the planned date on generated +delivery orders. + +======= +License +======= + +Please see `LICENSE `_. + +Copyright Hibou Corp. 2018 diff --git a/sale_sourced_by_line/__init__.py b/sale_sourced_by_line/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/sale_sourced_by_line/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/sale_sourced_by_line/__manifest__.py b/sale_sourced_by_line/__manifest__.py new file mode 100644 index 00000000..1bf80449 --- /dev/null +++ b/sale_sourced_by_line/__manifest__.py @@ -0,0 +1,29 @@ +{ + 'name': 'Sale Sourced by Line', + 'summary': 'Multiple warehouse source locations for Sale order', + 'version': '11.0.1.0.0', + 'author': "Hibou Corp.,Odoo Community Association (OCA)", + 'category': 'Warehouse', + 'license': 'AGPL-3', + 'complexity': 'expert', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +Sale Sourced by Line +==================== + +Adds the possibility to source a line of sale order from a specific +warehouse instead of using the warehouse of the sale order. + +""", + 'depends': [ + 'sale_stock', + 'sale_order_dates', + ], + 'demo': [], + 'data': [ + 'views/sale_views.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/sale_sourced_by_line/models/__init__.py b/sale_sourced_by_line/models/__init__.py new file mode 100644 index 00000000..8a0dc04e --- /dev/null +++ b/sale_sourced_by_line/models/__init__.py @@ -0,0 +1 @@ +from . import sale diff --git a/sale_sourced_by_line/models/sale.py b/sale_sourced_by_line/models/sale.py new file mode 100644 index 00000000..ea2b438d --- /dev/null +++ b/sale_sourced_by_line/models/sale.py @@ -0,0 +1,25 @@ +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + date_planned = fields.Datetime('Planned Date') + + +class SaleOrderLine(models.Model): + _inherit = 'sale.order.line' + + warehouse_id = fields.Many2one('stock.warehouse', string='Warehouse') + date_planned = fields.Datetime('Planned Date') + + @api.multi + def _prepare_procurement_values(self, group_id=False): + vals = super(SaleOrderLine, self)._prepare_procurement_values(group_id=group_id) + if self.warehouse_id: + vals.update({'warehouse_id': self.warehouse_id}) + if self.date_planned: + vals.update({'date_planned': self.date_planned}) + elif self.order_id.date_planned: + vals.update({'date_planned': self.order_id.date_planned}) + return vals diff --git a/sale_sourced_by_line/tests/__init__.py b/sale_sourced_by_line/tests/__init__.py new file mode 100644 index 00000000..90197d28 --- /dev/null +++ b/sale_sourced_by_line/tests/__init__.py @@ -0,0 +1 @@ +from . import test_sale_sources diff --git a/sale_sourced_by_line/tests/test_sale_sources.py b/sale_sourced_by_line/tests/test_sale_sources.py new file mode 100644 index 00000000..2038d24a --- /dev/null +++ b/sale_sourced_by_line/tests/test_sale_sources.py @@ -0,0 +1,23 @@ +from odoo.tests import common + + +class TestSaleSources(common.TransactionCase): + + def test_plan_two_warehouses(self): + partner = self.env.ref('base.res_partner_2') + product_1 = self.env.ref('product.product_product_24_product_template') + product_2 = self.env.ref('product.product_product_16_product_template') + wh_1 = self.env.ref('stock.stock_warehouse_shop0') + wh_2 = self.env.ref('stock.warehouse0') + so = self.env['sale.order'].create({ + 'warehouse_id': wh_1.id, + 'partner_id': partner.id, + 'date_planned': '2018-01-01', + 'order_line': [(0, 0, {'product_id': product_1.product_variant_id.id}), + (0, 0, {'product_id': product_2.product_variant_id.id, 'date_planned': '2018-02-01', 'warehouse_id': wh_2.id})] + }) + so.action_confirm() + self.assertTrue(so.state in ('sale', 'done')) + self.assertEqual(len(so.picking_ids), 2) + self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == wh_1)), 1) + self.assertEqual(len(so.picking_ids.filtered(lambda p: p.picking_type_id.warehouse_id == wh_2)), 1) diff --git a/sale_sourced_by_line/views/sale_views.xml b/sale_sourced_by_line/views/sale_views.xml new file mode 100644 index 00000000..05d68816 --- /dev/null +++ b/sale_sourced_by_line/views/sale_views.xml @@ -0,0 +1,32 @@ + + + + sale.order.form.warehouse + sale.order + + + + + + + + + + + + + + + + + sale.order.line.tree.warehouse + sale.order.line + + + + + + + + + \ No newline at end of file From 5892be202a2ecab8e21da5fd8adb9eceb1a2dac0 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 9 May 2018 09:27:45 -0700 Subject: [PATCH 02/44] Initial commit of `sale_planner` for 11.0 --- sale_planner/__init__.py | 2 + sale_planner/__manifest__.py | 40 ++ sale_planner/models/__init__.py | 3 + sale_planner/models/delivery.py | 69 +++ sale_planner/models/sale.py | 16 + sale_planner/models/stock.py | 9 + sale_planner/tests/__init__.py | 1 + sale_planner/tests/test_planner.py | 204 +++++++ sale_planner/views/delivery.xml | 13 + sale_planner/views/sale.xml | 17 + sale_planner/views/stock.xml | 13 + sale_planner/wizard/__init__.py | 1 + sale_planner/wizard/order_planner.py | 590 ++++++++++++++++++++ sale_planner/wizard/order_planner_views.xml | 40 ++ 14 files changed, 1018 insertions(+) create mode 100644 sale_planner/__init__.py create mode 100644 sale_planner/__manifest__.py create mode 100644 sale_planner/models/__init__.py create mode 100644 sale_planner/models/delivery.py create mode 100644 sale_planner/models/sale.py create mode 100644 sale_planner/models/stock.py create mode 100644 sale_planner/tests/__init__.py create mode 100644 sale_planner/tests/test_planner.py create mode 100644 sale_planner/views/delivery.xml create mode 100644 sale_planner/views/sale.xml create mode 100644 sale_planner/views/stock.xml create mode 100644 sale_planner/wizard/__init__.py create mode 100644 sale_planner/wizard/order_planner.py create mode 100644 sale_planner/wizard/order_planner_views.xml diff --git a/sale_planner/__init__.py b/sale_planner/__init__.py new file mode 100644 index 00000000..134df274 --- /dev/null +++ b/sale_planner/__init__.py @@ -0,0 +1,2 @@ +from . import wizard +from . import models diff --git a/sale_planner/__manifest__.py b/sale_planner/__manifest__.py new file mode 100644 index 00000000..a6bddc5c --- /dev/null +++ b/sale_planner/__manifest__.py @@ -0,0 +1,40 @@ +{ + 'name': 'Sale Order Planner', + 'summary': 'Plans order dates and warehouses.', + 'version': '11.0.1.0.0', + 'author': "Hibou Corp.", + 'category': 'Sale', + 'license': 'AGPL-3', + 'complexity': 'expert', + 'images': [], + 'website': "https://hibou.io", + 'description': """ +Sale Order Planner +================== + +Plans sales order dates based on available warehouses and shipping methods. + +Adds shipping calendar to warehouse to plan delivery orders based on availability +of the warehouse or warehouse staff. + +Adds shipping calendar to individual shipping methods to estimate delivery based +on the specific method's characteristics. (e.g. Do they deliver on Saturday?) + + +""", + 'depends': [ + 'sale_order_dates', + 'sale_sourced_by_line', + 'base_geolocalize', + 'delivery', + ], + 'demo': [], + 'data': [ + 'wizard/order_planner_views.xml', + 'views/sale.xml', + 'views/stock.xml', + 'views/delivery.xml', + ], + 'auto_install': False, + 'installable': True, +} diff --git a/sale_planner/models/__init__.py b/sale_planner/models/__init__.py new file mode 100644 index 00000000..35dedabf --- /dev/null +++ b/sale_planner/models/__init__.py @@ -0,0 +1,3 @@ +from . import sale +from . import stock +from . import delivery diff --git a/sale_planner/models/delivery.py b/sale_planner/models/delivery.py new file mode 100644 index 00000000..b46cebcb --- /dev/null +++ b/sale_planner/models/delivery.py @@ -0,0 +1,69 @@ +from datetime import timedelta + +from odoo import api, fields, models + + +class DeliveryCarrier(models.Model): + _inherit = 'delivery.carrier' + + delivery_calendar_id = fields.Many2one( + 'resource.calendar', 'Delivery Calendar', + help="This calendar represents days that the carrier will deliver the package.") + + # -------------------------- # + # API for external providers # + # -------------------------- # + + def get_shipping_price_for_plan(self, orders, date_planned): + ''' For every sale order, compute the price of the shipment + + :param orders: A recordset of sale orders + :param date_planned: Date to say that the shipment is leaving. + :return list: A list of floats, containing the estimated price for the shipping of the sale order + ''' + self.ensure_one() + if hasattr(self, '%s_get_shipping_price_for_plan' % self.delivery_type): + return getattr(self, '%s_get_shipping_price_for_plan' % self.delivery_type)(orders, date_planned) + + def calculate_transit_days(self, date_planned, date_delivered): + self.ensure_one() + if isinstance(date_planned, str): + date_planned = fields.Datetime.from_string(date_planned) + if isinstance(date_delivered, str): + date_delivered = fields.Datetime.from_string(date_delivered) + + transit_days = 0 + while date_planned < date_delivered: + if transit_days > 10: + break + interval = self.delivery_calendar_id.schedule_days(1, date_planned, compute_leaves=True) + + if not interval: + return self._calculate_transit_days_naive(date_planned, date_delivered) + date_planned = interval[0][1] + transit_days += 1 + + if transit_days > 1: + transit_days -= 1 + + return transit_days + + def _calculate_transit_days_naive(self, date_planned, date_delivered): + return abs((date_delivered - date_planned).days) + + def calculate_date_delivered(self, date_planned, transit_days): + self.ensure_one() + if isinstance(date_planned, str): + date_planned = fields.Datetime.from_string(date_planned) + + # date calculations needs an extra day + effective_transit_days = transit_days + 1 + + interval = self.delivery_calendar_id.schedule_days(effective_transit_days, date_planned, compute_leaves=True) + if not interval: + return self._calculate_date_delivered_naive(date_planned, transit_days) + + return fields.Datetime.to_string(interval[-1][1]) + + def _calculate_date_delivered_naive(self, date_planned, transit_days): + return fields.Datetime.to_string(date_planned + timedelta(days=transit_days)) diff --git a/sale_planner/models/sale.py b/sale_planner/models/sale.py new file mode 100644 index 00000000..6d5e46b0 --- /dev/null +++ b/sale_planner/models/sale.py @@ -0,0 +1,16 @@ +from odoo import api, fields, models + + +class SaleOrder(models.Model): + _inherit = 'sale.order' + + @api.multi + def action_planorder(self): + plan_obj = self.env['sale.order.make.plan'] + for order in self: + plan = plan_obj.create({ + 'order_id': order.id, + }) + action = self.env.ref('sale_planner.action_plan_sale_order').read()[0] + action['res_id'] = plan.id + return action diff --git a/sale_planner/models/stock.py b/sale_planner/models/stock.py new file mode 100644 index 00000000..4d72f4f4 --- /dev/null +++ b/sale_planner/models/stock.py @@ -0,0 +1,9 @@ +from odoo import api, fields, models + + +class Warehouse(models.Model): + _inherit = 'stock.warehouse' + + shipping_calendar_id = fields.Many2one( + 'resource.calendar', 'Shipping Calendar', + help="This calendar represents shipping availability from the warehouse.") diff --git a/sale_planner/tests/__init__.py b/sale_planner/tests/__init__.py new file mode 100644 index 00000000..25366b57 --- /dev/null +++ b/sale_planner/tests/__init__.py @@ -0,0 +1 @@ +from . import test_planner diff --git a/sale_planner/tests/test_planner.py b/sale_planner/tests/test_planner.py new file mode 100644 index 00000000..b2d3bd61 --- /dev/null +++ b/sale_planner/tests/test_planner.py @@ -0,0 +1,204 @@ +from odoo.tests import common +from datetime import datetime, timedelta + + +class TestPlanner(common.TransactionCase): + # @todo Test date planning! + + def setUp(self): + super(TestPlanner, self).setUp() + self.today = datetime.today() + self.tomorrow = datetime.today() + timedelta(days=1) + # This partner has a parent + self.country_usa = self.env['res.country'].search([('name', '=', 'United States')], limit=1) + self.state_wa = self.env['res.country.state'].search([('name', '=', 'Washington')], limit=1) + self.state_co = self.env['res.country.state'].search([('name', '=', 'Colorado')], limit=1) + self.partner_wa = self.env['res.partner'].create({ + 'name': 'Jared', + 'street': '1234 Test Street', + 'city': 'Marysville', + 'state_id': self.state_wa.id, + 'zip': '98270', + 'country_id': self.country_usa.id, + 'partner_latitude': 48.05636, + 'partner_longitude': -122.14922, + }) + self.warehouse_partner_1 = self.env['res.partner'].create({ + 'name': 'WH1', + 'street': '1234 Test Street', + 'city': 'Lynnwood', + 'state_id': self.state_wa.id, + 'zip': '98036', + 'country_id': self.country_usa.id, + 'partner_latitude': 47.82093, + 'partner_longitude': -122.31513, + }) + + self.warehouse_partner_2 = self.env['res.partner'].create({ + 'name': 'WH2', + 'street': '1234 Test Street', + 'city': 'Craig', + 'state_id': self.state_co.id, + 'zip': '81625', + 'country_id': self.country_usa.id, + 'partner_latitude': 40.51525, + 'partner_longitude': -107.54645, + }) + self.warehouse_calendar_1 = self.env['resource.calendar'].create({ + 'name': 'Washington Warehouse Hours', + 'attendance_ids': [ + (0, 0, {'name': 'today', + 'dayofweek': str(self.today.weekday()), + 'hour_from': (self.today.hour - 1) % 24, + 'hour_to': (self.today.hour + 1) % 24}), + (0, 0, {'name': 'tomorrow', + 'dayofweek': str(self.tomorrow.weekday()), + 'hour_from': (self.tomorrow.hour - 1) % 24, + 'hour_to': (self.tomorrow.hour + 1) % 24}), + ] + }) + self.warehouse_calendar_2 = self.env['resource.calendar'].create({ + 'name': 'Colorado Warehouse Hours', + 'attendance_ids': [ + (0, 0, {'name': 'tomorrow', + 'dayofweek': str(self.tomorrow.weekday()), + 'hour_from': (self.tomorrow.hour - 1) % 24, + 'hour_to': (self.tomorrow.hour + 1) % 24}), + ] + }) + self.warehouse_1 = self.env['stock.warehouse'].create({ + 'name': 'Washington Warehouse', + 'partner_id': self.warehouse_partner_1.id, + 'code': 'WH1', + 'shipping_calendar_id': self.warehouse_calendar_1.id, + }) + self.warehouse_2 = self.env['stock.warehouse'].create({ + 'name': 'Colorado Warehouse', + 'partner_id': self.warehouse_partner_2.id, + 'code': 'WH2', + 'shipping_calendar_id': self.warehouse_calendar_2.id, + }) + self.so = self.env['sale.order'].create({ + 'partner_id': self.partner_wa.id, + 'warehouse_id': self.warehouse_1.id, + }) + self.product_1 = self.env['product.template'].create({ + 'name': 'Product for WH1', + 'type': 'product', + 'standard_price': 1.0, + }) + self.product_1 = self.product_1.product_variant_id + self.product_2 = self.env['product.template'].create({ + 'name': 'Product for WH2', + 'type': 'product', + 'standard_price': 1.0, + }) + self.product_2 = self.product_2.product_variant_id + self.product_both = self.env['product.template'].create({ + 'name': 'Product for Both', + 'type': 'product', + 'standard_price': 1.0, + }) + self.product_both = self.product_both.product_variant_id + self.env['stock.change.product.qty'].create({ + 'location_id': self.warehouse_1.lot_stock_id.id, + 'product_id': self.product_1.id, + 'new_quantity': 100, + }).change_product_qty() + self.env['stock.change.product.qty'].create({ + 'location_id': self.warehouse_1.lot_stock_id.id, + 'product_id': self.product_both.id, + 'new_quantity': 100, + }).change_product_qty() + self.env['stock.change.product.qty'].create({ + 'location_id': self.warehouse_2.lot_stock_id.id, + 'product_id': self.product_2.id, + 'new_quantity': 100, + }).change_product_qty() + self.env['stock.change.product.qty'].create({ + 'location_id': self.warehouse_2.lot_stock_id.id, + 'product_id': self.product_both.id, + 'new_quantity': 100, + }).change_product_qty() + + def both_wh_ids(self): + return [self.warehouse_1.id, self.warehouse_2.id] + + def test_planner_creation_internals(self): + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_1.id, + 'name': 'demo', + }) + both_wh_ids = self.both_wh_ids() + planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) + self.assertEqual(set(both_wh_ids), set(planner.get_warehouses().ids)) + fake_order = planner._fake_order(self.so) + base_option = planner.generate_base_option(fake_order) + self.assertTrue(base_option, 'Must have base option.') + self.assertEqual(self.warehouse_1.id, base_option['warehouse_id']) + + def test_planner_creation(self): + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_1.id, + 'name': 'demo', + }) + self.assertEqual(self.product_1.with_context(warehouse=self.warehouse_1.id).qty_available, 100) + self.assertEqual(self.product_1.with_context(warehouse=self.warehouse_2.id).qty_available, 0) + both_wh_ids = self.both_wh_ids() + planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) + self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') + self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_1) + self.assertFalse(planner.planning_option_ids[0].sub_options) + + def test_planner_creation_2(self): + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_2.id, + 'name': 'demo', + }) + self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_1.id).qty_available, 0) + self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_2.id).qty_available, 100) + both_wh_ids = self.both_wh_ids() + planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) + self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') + self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2) + self.assertFalse(planner.planning_option_ids[0].sub_options) + + def test_planner_creation_split(self): + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_1.id, + 'name': 'demo', + }) + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_2.id, + 'name': 'demo', + }) + self.assertEqual(self.product_1.with_context(warehouse=self.warehouse_1.id).qty_available, 100) + self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_2.id).qty_available, 100) + both_wh_ids = self.both_wh_ids() + planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) + self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') + self.assertTrue(planner.planning_option_ids[0].sub_options) + + def test_planner_creation_no_split(self): + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_both.id, + 'name': 'demo', + }) + self.env['sale.order.line'].create({ + 'order_id': self.so.id, + 'product_id': self.product_2.id, + 'name': 'demo', + }) + self.assertEqual(self.product_both.with_context(warehouse=self.warehouse_2.id).qty_available, 100) + self.assertEqual(self.product_2.with_context(warehouse=self.warehouse_2.id).qty_available, 100) + both_wh_ids = self.both_wh_ids() + planner = self.env['sale.order.make.plan'].with_context(warehouse_domain=[('id', 'in', both_wh_ids)], skip_plan_shipping=True).create({'order_id': self.so.id}) + self.assertTrue(planner.planning_option_ids, 'Must have one or more plans.') + self.assertEqual(planner.planning_option_ids.warehouse_id, self.warehouse_2) + self.assertFalse(planner.planning_option_ids[0].sub_options) diff --git a/sale_planner/views/delivery.xml b/sale_planner/views/delivery.xml new file mode 100644 index 00000000..16e36513 --- /dev/null +++ b/sale_planner/views/delivery.xml @@ -0,0 +1,13 @@ + + + + delivery.carrier.form.calendar + delivery.carrier + + + + + + + + \ No newline at end of file diff --git a/sale_planner/views/sale.xml b/sale_planner/views/sale.xml new file mode 100644 index 00000000..aec860a2 --- /dev/null +++ b/sale_planner/views/sale.xml @@ -0,0 +1,17 @@ + + + + sale.order.form.planner + sale.order + + + + + + + + \ No newline at end of file diff --git a/hr_expense_lead/views/hr_expense_views.xml b/hr_expense_lead/views/hr_expense_views.xml new file mode 100644 index 00000000..7bb69189 --- /dev/null +++ b/hr_expense_lead/views/hr_expense_views.xml @@ -0,0 +1,24 @@ + + + + hr.expense.form.inherit + hr.expense + + + + + + + + + + hr.expense.filter.inherit + hr.expense + + + + + + + + \ No newline at end of file From fa68c2ea9431270f3aa1f2c1a09aeb7d0ef1fa9f Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 16 May 2018 11:43:08 -0700 Subject: [PATCH 13/44] Move 'Lead' field on Expense form. --- hr_expense_lead/views/hr_expense_views.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hr_expense_lead/views/hr_expense_views.xml b/hr_expense_lead/views/hr_expense_views.xml index 7bb69189..be8ef68a 100644 --- a/hr_expense_lead/views/hr_expense_views.xml +++ b/hr_expense_lead/views/hr_expense_views.xml @@ -5,7 +5,7 @@ hr.expense - + From 2b301bbfe2626179a6541132578c770936a295b9 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Mon, 21 May 2018 17:31:57 -0700 Subject: [PATCH 14/44] Initial commit of Newrelic addon from https://github.com/hibou-io/odoo-newrelic for 11.0 --- newrelic/__init__.py | 71 ++++++++++++++++++++++++++++++++ newrelic/__manifest__.py | 12 ++++++ newrelic/controllers/__init__.py | 1 + newrelic/controllers/main.py | 39 ++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 newrelic/__init__.py create mode 100644 newrelic/__manifest__.py create mode 100644 newrelic/controllers/__init__.py create mode 100644 newrelic/controllers/main.py diff --git a/newrelic/__init__.py b/newrelic/__init__.py new file mode 100644 index 00000000..39a2e959 --- /dev/null +++ b/newrelic/__init__.py @@ -0,0 +1,71 @@ +from . import controllers + +from logging import getLogger +_logger = getLogger(__name__) + +try: + import odoo + target = odoo.service.server.server + + try: + instrumented = target._nr_instrumented + except AttributeError: + instrumented = target._nr_instrumented = False + + if instrumented: + _logger.info("NewRelic instrumented already") + else: + import odoo.tools.config as config + import newrelic.agent + + + try: + newrelic.agent.initialize(config['new_relic_config_file'], config['new_relic_environment']) + except KeyError: + try: + newrelic.agent.initialize(config['new_relic_config_file']) + except KeyError: + _logger.info('NewRelic setting up from env variables') + newrelic.agent.initialize() + + # Main WSGI Application + target._nr_instrumented = True + target.app = newrelic.agent.WSGIApplicationWrapper(target.app) + + # Workers new WSGI Application + target = odoo.service.wsgi_server + target.application_unproxied = newrelic.agent.WSGIApplicationWrapper(target.application_unproxied) + + # Error handling + def should_ignore(exc, value, tb): + from werkzeug.exceptions import HTTPException + + # Werkzeug HTTPException can be raised internally by Odoo or in + # user code if they mix Odoo with Werkzeug. Filter based on the + # HTTP status code. + + if isinstance(value, HTTPException): + if newrelic.agent.ignore_status_code(value.code): + return True + + def _nr_wrapper_handle_exception_(wrapped): + def _handle_exception(*args, **kwargs): + transaction = newrelic.agent.current_transaction() + + if transaction is None: + return wrapped(*args, **kwargs) + + transaction.record_exception(ignore_errors=should_ignore) + + name = newrelic.agent.callable_name(args[1]) + with newrelic.agent.FunctionTrace(transaction, name): + return wrapped(*args, **kwargs) + + return _handle_exception + + target = odoo.http.WebRequest + target._handle_exception = _nr_wrapper_handle_exception_(target._handle_exception) + + +except ImportError: + _logger.warn('newrelic python module not installed or other missing module') \ No newline at end of file diff --git a/newrelic/__manifest__.py b/newrelic/__manifest__.py new file mode 100644 index 00000000..d842276c --- /dev/null +++ b/newrelic/__manifest__.py @@ -0,0 +1,12 @@ +{ + 'name': 'NewRelic Instrumentation', + 'description': 'Wraps requests etc.', + 'version': '1.0', + 'website': 'https://hibou.io/', + 'author': 'Hibou Corp. ', + 'license': 'AGPL-3', + 'category': 'Tool', + 'depends': [ + 'web', + ], +} diff --git a/newrelic/controllers/__init__.py b/newrelic/controllers/__init__.py new file mode 100644 index 00000000..12a7e529 --- /dev/null +++ b/newrelic/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/newrelic/controllers/main.py b/newrelic/controllers/main.py new file mode 100644 index 00000000..acd1dccb --- /dev/null +++ b/newrelic/controllers/main.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- + +from odoo import http, tools +import odoo.addons.bus.controllers.main + +try: + import newrelic + import newrelic.agent +except ImportError: + newrelic = None + + +class BusController(odoo.addons.bus.controllers.main.BusController): + + @http.route() + def send(self, channel, message): + if newrelic: + newrelic.agent.ignore_transaction() + return super(BusController, self).send(channel, message) + + @http.route() + def poll(self, channels, last, options=None): + if newrelic: + newrelic.agent.ignore_transaction() + return super(BusController, self).poll(channels, last, options) + +try: + if tools.config['debug_mode']: + class TestErrors(http.Controller): + @http.route('/test_errors_404', auth='public') + def test_errors_404(self): + import werkzeug + return werkzeug.exceptions.NotFound('Successful test of 404') + + @http.route('/test_errors_500', auth='public') + def test_errors_500(self): + raise ValueError +except KeyError: + pass From 453c795a8c9c549c5f6fcad48f7c6737f510698e Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 23 May 2018 11:43:00 -0700 Subject: [PATCH 15/44] Initial commit of `hr_employee_activity` for 11.0 -- Adds 'activity' to Employee records. (shouldn't backport) --- hr_employee_activity/__init__.py | 1 + hr_employee_activity/__manifest__.py | 21 +++++++++++++++++++ hr_employee_activity/hr_employee_activity.py | 6 ++++++ .../hr_employee_activity_views.xml | 13 ++++++++++++ 4 files changed, 41 insertions(+) create mode 100644 hr_employee_activity/__init__.py create mode 100644 hr_employee_activity/__manifest__.py create mode 100644 hr_employee_activity/hr_employee_activity.py create mode 100644 hr_employee_activity/hr_employee_activity_views.xml diff --git a/hr_employee_activity/__init__.py b/hr_employee_activity/__init__.py new file mode 100644 index 00000000..ea0b1c0a --- /dev/null +++ b/hr_employee_activity/__init__.py @@ -0,0 +1 @@ +from . import hr_employee_activity diff --git a/hr_employee_activity/__manifest__.py b/hr_employee_activity/__manifest__.py new file mode 100644 index 00000000..14640db7 --- /dev/null +++ b/hr_employee_activity/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'HR Employee Activity', + 'version': '11.0.1.0.0', + 'author': 'Hibou Corp. ', + 'website': 'https://hibou.io/', + 'license': 'AGPL-3', + 'category': 'Employees', + 'complexity': 'easy', + 'description': """ +This module adds activity to the `hr.employee` model. + """, + 'depends': [ + 'hr', + ], + 'data': [ + 'hr_employee_activity_views.xml', + ], + 'installable': True, + 'auto_install': False, + 'category': 'Hidden', +} diff --git a/hr_employee_activity/hr_employee_activity.py b/hr_employee_activity/hr_employee_activity.py new file mode 100644 index 00000000..a1ce0e36 --- /dev/null +++ b/hr_employee_activity/hr_employee_activity.py @@ -0,0 +1,6 @@ +from odoo import models + + +class HrEmployee(models.Model): + _name = 'hr.employee' + _inherit = ['hr.employee', 'mail.activity.mixin'] diff --git a/hr_employee_activity/hr_employee_activity_views.xml b/hr_employee_activity/hr_employee_activity_views.xml new file mode 100644 index 00000000..09d27740 --- /dev/null +++ b/hr_employee_activity/hr_employee_activity_views.xml @@ -0,0 +1,13 @@ + + + + hr.employee.form.activity + hr.employee + + + + + + + + \ No newline at end of file From 84fb41e43ae757267406b05e146463c6cf97ef2b Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 23 May 2018 11:44:16 -0700 Subject: [PATCH 16/44] Initial commit of `project_description` for 11.0 -- Project level notes available on every task for reference. --- project_description/__init__.py | 1 + project_description/__manifest__.py | 22 +++++++++++++++ project_description/models/__init__.py | 1 + project_description/models/project.py | 12 +++++++++ project_description/views/project_views.xml | 30 +++++++++++++++++++++ 5 files changed, 66 insertions(+) create mode 100644 project_description/__init__.py create mode 100644 project_description/__manifest__.py create mode 100644 project_description/models/__init__.py create mode 100644 project_description/models/project.py create mode 100644 project_description/views/project_views.xml diff --git a/project_description/__init__.py b/project_description/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/project_description/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/project_description/__manifest__.py b/project_description/__manifest__.py new file mode 100644 index 00000000..19513c2a --- /dev/null +++ b/project_description/__manifest__.py @@ -0,0 +1,22 @@ +{ + 'name': 'Project Description', + 'version': '11.0.1.0.0', + 'author': 'Hibou Corp. ', + 'website': 'https://hibou.io/', + 'license': 'AGPL-3', + 'category': 'Tools', + 'complexity': 'easy', + 'description': """ +Adds description onto Projects that will be displayed on tasks. +Useful for keeping project specific notes that are needed whenever +you're working on a task in that project. + """, + 'depends': [ + 'project', + ], + 'data': [ + 'views/project_views.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/project_description/models/__init__.py b/project_description/models/__init__.py new file mode 100644 index 00000000..351a3ad3 --- /dev/null +++ b/project_description/models/__init__.py @@ -0,0 +1 @@ +from . import project diff --git a/project_description/models/project.py b/project_description/models/project.py new file mode 100644 index 00000000..371bd248 --- /dev/null +++ b/project_description/models/project.py @@ -0,0 +1,12 @@ +from odoo import api, fields, models + + +class Project(models.Model): + _inherit = 'project.project' + + note = fields.Html(string='Note') + +class ProjectTask(models.Model): + _inherit = 'project.task' + + project_note = fields.Html(related='project_id.note') diff --git a/project_description/views/project_views.xml b/project_description/views/project_views.xml new file mode 100644 index 00000000..6fb6efcd --- /dev/null +++ b/project_description/views/project_views.xml @@ -0,0 +1,30 @@ + + + + project.project.form.inherit + project.project + + + + + +
+ + + + + + + project.task.form.inherit + project.task + + + + + +
+ + + + + \ No newline at end of file From 0a21d042d1e065bffc07c1f8a2afbb47cd8420e3 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 23 May 2018 11:47:37 -0700 Subject: [PATCH 17/44] Initial commit of `timesheet_description` and `timesheet_description_sale` for 11.0 -- Forces popup for timesheet (detail) and allows you to display them as Markdown in the portal. --- timesheet_description/__init__.py | 1 + timesheet_description/__manifest__.py | 24 ++++++++++ timesheet_description/models/__init__.py | 1 + timesheet_description/models/timesheet.py | 23 ++++++++++ .../views/project_templates.xml | 9 ++++ .../views/timesheet_views.xml | 46 +++++++++++++++++++ timesheet_description_sale/__init__.py | 1 + timesheet_description_sale/__manifest__.py | 21 +++++++++ .../views/timesheet_views.xml | 19 ++++++++ 9 files changed, 145 insertions(+) create mode 100644 timesheet_description/__init__.py create mode 100644 timesheet_description/__manifest__.py create mode 100644 timesheet_description/models/__init__.py create mode 100644 timesheet_description/models/timesheet.py create mode 100644 timesheet_description/views/project_templates.xml create mode 100644 timesheet_description/views/timesheet_views.xml create mode 100644 timesheet_description_sale/__init__.py create mode 100644 timesheet_description_sale/__manifest__.py create mode 100644 timesheet_description_sale/views/timesheet_views.xml diff --git a/timesheet_description/__init__.py b/timesheet_description/__init__.py new file mode 100644 index 00000000..0650744f --- /dev/null +++ b/timesheet_description/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/timesheet_description/__manifest__.py b/timesheet_description/__manifest__.py new file mode 100644 index 00000000..2bc871a1 --- /dev/null +++ b/timesheet_description/__manifest__.py @@ -0,0 +1,24 @@ +{ + 'name': 'Timesheet Description', + 'version': '11.0.1.0.0', + 'author': 'Hibou Corp. ', + 'website': 'https://hibou.io/', + 'license': 'AGPL-3', + 'category': 'Tools', + 'complexity': 'easy', + 'description': """ +Timesheet entries will be made in a form view, allowing the end user to enter more descriptive timesheet entries. + +Optionally, allows you to display your timesheet entries in markdown on the front end of the website. + """, + 'depends': [ + 'project', + 'hr_timesheet', + ], + 'data': [ + 'views/project_templates.xml', + 'views/timesheet_views.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/timesheet_description/models/__init__.py b/timesheet_description/models/__init__.py new file mode 100644 index 00000000..1b7bb1fb --- /dev/null +++ b/timesheet_description/models/__init__.py @@ -0,0 +1 @@ +from . import timesheet diff --git a/timesheet_description/models/timesheet.py b/timesheet_description/models/timesheet.py new file mode 100644 index 00000000..30cd3401 --- /dev/null +++ b/timesheet_description/models/timesheet.py @@ -0,0 +1,23 @@ +try: + from markdown import markdown +except ImportError: + markdown = None + +from odoo import api, fields, models + + +class AnalyticLine(models.Model): + _inherit = 'account.analytic.line' + + name_markdown = fields.Html(compute='_compute_name_markdown') + + @api.multi + def _compute_name_markdown(self): + if not markdown: + for line in self: + # Why not just name? Because it needs to be escaped. + # Use nothing to indicate that it shouldn't be used. + line.name_markdown = '' + else: + for line in self: + line.name_markdown = markdown(line.name) diff --git a/timesheet_description/views/project_templates.xml b/timesheet_description/views/project_templates.xml new file mode 100644 index 00000000..e2d78d0b --- /dev/null +++ b/timesheet_description/views/project_templates.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/timesheet_description/views/timesheet_views.xml b/timesheet_description/views/timesheet_views.xml new file mode 100644 index 00000000..640a616a --- /dev/null +++ b/timesheet_description/views/timesheet_views.xml @@ -0,0 +1,46 @@ + + + + project.task.form.inherit + project.task + + + + {'default_project_id': project_id, 'default_task_id': active_id} + + + + + +
+ + + + + + + + + + + + + + + +
+
+
+
+ + + account.analytic.line.tree.hr_timesheet.inherit + account.analytic.line + + + + + + + +
\ No newline at end of file diff --git a/timesheet_description_sale/__init__.py b/timesheet_description_sale/__init__.py new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/timesheet_description_sale/__init__.py @@ -0,0 +1 @@ + diff --git a/timesheet_description_sale/__manifest__.py b/timesheet_description_sale/__manifest__.py new file mode 100644 index 00000000..122676b3 --- /dev/null +++ b/timesheet_description_sale/__manifest__.py @@ -0,0 +1,21 @@ +{ + 'name': 'Timesheet Description Sale', + 'version': '11.0.1.0.0', + 'author': 'Hibou Corp. ', + 'website': 'https://hibou.io/', + 'license': 'AGPL-3', + 'category': 'Tools', + 'complexity': 'easy', + 'description': """ +Linker module to inject fields required by timesheets for sales. + """, + 'depends': [ + 'timesheet_description', + 'sale_timesheet', + ], + 'data': [ + 'views/timesheet_views.xml', + ], + 'installable': True, + 'auto_install': True, +} diff --git a/timesheet_description_sale/views/timesheet_views.xml b/timesheet_description_sale/views/timesheet_views.xml new file mode 100644 index 00000000..f4d48acd --- /dev/null +++ b/timesheet_description_sale/views/timesheet_views.xml @@ -0,0 +1,19 @@ + + + + project.task.form.inherit + project.task + + + + {'default_project_id': project_id, 'default_task_id': active_id} + + + + + + + + + + \ No newline at end of file From a5ff0328d7919989a9c3e5fd8f8194175c6d3e28 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 23 May 2018 11:49:49 -0700 Subject: [PATCH 18/44] Initial commit of `timesheet_invoice` for 11.0 -- Include detailed timesheet entries on invoice PDF. --- timesheet_invoice/__init__.py | 0 timesheet_invoice/__manifest__.py | 20 +++++++++++++++++ timesheet_invoice/invoice_views.xml | 33 +++++++++++++++++++++++++++++ 3 files changed, 53 insertions(+) create mode 100644 timesheet_invoice/__init__.py create mode 100644 timesheet_invoice/__manifest__.py create mode 100644 timesheet_invoice/invoice_views.xml diff --git a/timesheet_invoice/__init__.py b/timesheet_invoice/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/timesheet_invoice/__manifest__.py b/timesheet_invoice/__manifest__.py new file mode 100644 index 00000000..78374c94 --- /dev/null +++ b/timesheet_invoice/__manifest__.py @@ -0,0 +1,20 @@ +{ + 'name': 'Timesheet Invoice', + 'version': '11.0.1.0.0', + 'author': 'Hibou Corp. ', + 'website': 'https://hibou.io/', + 'license': 'AGPL-3', + 'category': 'Tools', + 'complexity': 'easy', + 'description': """ +Adds timesheet descriptions onto the invoice report/PDF. + """, + 'depends': [ + 'sale_timesheet', + ], + 'data': [ + 'invoice_views.xml', + ], + 'installable': True, + 'auto_install': False, +} diff --git a/timesheet_invoice/invoice_views.xml b/timesheet_invoice/invoice_views.xml new file mode 100644 index 00000000..0f74f435 --- /dev/null +++ b/timesheet_invoice/invoice_views.xml @@ -0,0 +1,33 @@ + + + + \ No newline at end of file From 026b6199be2a7583a883847df31feac9c17b056b Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Wed, 23 May 2018 11:50:23 -0700 Subject: [PATCH 19/44] Upgrade `website_project_task` for 11.0 --- website_project_task/__init__.py | 0 website_project_task/__manifest__.py | 25 ++++++++++++ .../project_task_templates.xml | 38 +++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 website_project_task/__init__.py create mode 100644 website_project_task/__manifest__.py create mode 100644 website_project_task/project_task_templates.xml diff --git a/website_project_task/__init__.py b/website_project_task/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/website_project_task/__manifest__.py b/website_project_task/__manifest__.py new file mode 100644 index 00000000..efb95eed --- /dev/null +++ b/website_project_task/__manifest__.py @@ -0,0 +1,25 @@ +{ + 'name': 'Website Project Tasks', + 'version': '11.0.1.0.0', + 'author': 'Hibou Corp. ', + 'website': 'https://hibou.io/', + 'license': 'AGPL-3', + 'category': 'Tools', + 'complexity': 'easy', + 'description': """ +This module adds options to Website Project: +============================================ + +* Tags on Tasks +* Classes on Stages and Tags for CSS hooks. + """, + 'depends': [ + 'project', + ], + 'data': [ + 'project_task_templates.xml', + ], + 'installable': True, + 'auto_install': False, + 'category': 'Hidden', +} diff --git a/website_project_task/project_task_templates.xml b/website_project_task/project_task_templates.xml new file mode 100644 index 00000000..47fd67ac --- /dev/null +++ b/website_project_task/project_task_templates.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + From a90f7d939f7f0d1ea8667be3a0f5ad03afe3a260 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sun, 27 May 2018 13:00:30 -0700 Subject: [PATCH 20/44] Add `auth_admin` for 11.0 from https://github.com/hibou-io/odoo-auth-admin --- auth_admin/__init__.py | 4 + auth_admin/__manifest__.py | 30 ++++++++ auth_admin/controllers/__init__.py | 2 + auth_admin/controllers/main.py | 41 +++++++++++ auth_admin/models/__init__.py | 2 + auth_admin/models/res_users.py | 90 +++++++++++++++++++++++ auth_admin/views/res_users.xml | 14 ++++ auth_admin/wizard/__init__.py | 2 + auth_admin/wizard/portal_wizard.py | 27 +++++++ auth_admin/wizard/portal_wizard_views.xml | 18 +++++ 10 files changed, 230 insertions(+) create mode 100755 auth_admin/__init__.py create mode 100755 auth_admin/__manifest__.py create mode 100755 auth_admin/controllers/__init__.py create mode 100755 auth_admin/controllers/main.py create mode 100755 auth_admin/models/__init__.py create mode 100755 auth_admin/models/res_users.py create mode 100755 auth_admin/views/res_users.xml create mode 100755 auth_admin/wizard/__init__.py create mode 100755 auth_admin/wizard/portal_wizard.py create mode 100755 auth_admin/wizard/portal_wizard_views.xml diff --git a/auth_admin/__init__.py b/auth_admin/__init__.py new file mode 100755 index 00000000..cec04a5b --- /dev/null +++ b/auth_admin/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from . import controllers +from . import models +from . import wizard diff --git a/auth_admin/__manifest__.py b/auth_admin/__manifest__.py new file mode 100755 index 00000000..d2f49137 --- /dev/null +++ b/auth_admin/__manifest__.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- + +{ + 'name': 'Auth Admin', + 'author': 'Hibou Corp. ', + 'category': 'Hidden', + 'version': '11.0.0.0.0', + 'description': + """ +Login as other user +=================== + +Provides a way for an authenticated user, with certain permissions, to login as a different user. +Can also create a URL that logs in as that user. + +Out of the box, only allows you to generate a login for an 'External User', e.g. portal users. + +*2017-11-15* New button to generate the login on the Portal User Wizard (Action on Contact) + """, + 'depends': [ + 'base', + 'website', + 'portal', + ], + 'auto_install': False, + 'data': [ + 'views/res_users.xml', + 'wizard/portal_wizard_views.xml', + ], +} diff --git a/auth_admin/controllers/__init__.py b/auth_admin/controllers/__init__.py new file mode 100755 index 00000000..757b12a1 --- /dev/null +++ b/auth_admin/controllers/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import main diff --git a/auth_admin/controllers/main.py b/auth_admin/controllers/main.py new file mode 100755 index 00000000..13b2ec14 --- /dev/null +++ b/auth_admin/controllers/main.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- + +from odoo import http, exceptions +from ..models.res_users import check_admin_auth_login + +from logging import getLogger +_logger = getLogger(__name__) + + +class AuthAdmin(http.Controller): + + @http.route(['/auth_admin'], type='http', auth='public', website=True) + def index(self, *args, **post): + u = post.get('u') + e = post.get('e') + o = post.get('o') + h = post.get('h') + + if not all([u, e, o, h]): + exceptions.Warning('Invalid Request') + + u = str(u) + e = str(e) + o = str(o) + h = str(h) + + try: + user = check_admin_auth_login(http.request.env, u, e, o, h) + + http.request.session.uid = user.id + http.request.session.login = user.login + http.request.session.password = '' + http.request.session.auth_admin = int(o) + http.request.uid = user.id + uid = http.request.session.authenticate(http.request.session.db, user.login, 'x') + if uid is not False: + http.request.params['login_success'] = True + return http.redirect_with_hash('/my/home') + return http.local_redirect('/my/home') + except (exceptions.Warning, ) as e: + return http.Response(e.message, status=400) diff --git a/auth_admin/models/__init__.py b/auth_admin/models/__init__.py new file mode 100755 index 00000000..741ed460 --- /dev/null +++ b/auth_admin/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import res_users diff --git a/auth_admin/models/res_users.py b/auth_admin/models/res_users.py new file mode 100755 index 00000000..e3d0ffea --- /dev/null +++ b/auth_admin/models/res_users.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- + +from odoo import models, api, exceptions +from odoo.http import request +from datetime import datetime +from time import mktime +import hmac +from hashlib import sha256 + +from logging import getLogger +_logger = getLogger(__name__) + +def admin_auth_generate_login(env, user): + """ + Generates a URL to allow the current user to login as the portal user. + + :param env: Odoo environment + :param user: `res.users` in + :return: + """ + if not env['res.partner'].check_access_rights('write'): + return None + u = str(user.id) + now = datetime.utcnow() + fifteen = int(mktime(now.timetuple())) + (15 * 60) + e = str(fifteen) + o = str(env.uid) + + config = env['ir.config_parameter'].sudo() + key = str(config.search([('key', '=', 'database.secret')], limit=1).value) + h = hmac.new(key.encode(), (u + e + o).encode(), sha256) + + base_url = str(config.search([('key', '=', 'web.base.url')], limit=1).value) + + _logger.warn('login url for user id: ' + u + ' original user id: ' + o) + + return base_url + '/auth_admin?u=' + u + '&e=' + e + '&o=' + o + '&h=' + h.hexdigest() + +def check_admin_auth_login(env, u_user_id, e_expires, o_org_user_id, hash_): + """ + Checks that the parameters are valid and that the user exists. + + :param env: Odoo environment + :param u_user_id: Desired user id to login as. + :param e_expires: Expiration timestamp + :param o_org_user_id: Original user id. + :param hash_: HMAC generated hash + :return: `res.users` + """ + + now = datetime.utcnow() + now = int(mktime(now.timetuple())) + fifteen = now + (15 * 60) + + config = env['ir.config_parameter'].sudo() + key = str(config.search([('key', '=', 'database.secret')], limit=1).value) + + myh = hmac.new(key.encode(), str(str(u_user_id) + str(e_expires) + str(o_org_user_id)).encode(), sha256) + + if not hmac.compare_digest(hash_, myh.hexdigest()): + raise exceptions.Warning('Invalid Request') + + if not (now <= int(e_expires) <= fifteen): + raise exceptions.Warning('Expired') + + user = env['res.users'].sudo().search([('id', '=', int(u_user_id))], limit=1) + if not user.id: + raise exceptions.Warning('Invalid User') + return user + + +class ResUsers(models.Model): + _inherit = 'res.users' + + @api.multi + def admin_auth_generate_login(self): + self.ensure_one() + + login_url = admin_auth_generate_login(self.env, self) + if login_url: + raise exceptions.Warning(login_url) + + return False + + @api.model + def check_credentials(self, password): + if request and hasattr(request, 'session') and request.session.get('auth_admin'): + _logger.warn('check_credentials for user id: ' + str(request.session.uid) + ' original user id: ' + str(request.session.auth_admin)) + return True + return super(ResUsers, self).check_credentials(password) diff --git a/auth_admin/views/res_users.xml b/auth_admin/views/res_users.xml new file mode 100755 index 00000000..b818c870 --- /dev/null +++ b/auth_admin/views/res_users.xml @@ -0,0 +1,14 @@ + + + + auth_admin.res.users.tree + res.users + + + + + From bb00f622df76dbdae0541079f95fdde656129e84 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Fri, 29 Jun 2018 15:38:48 -0700 Subject: [PATCH 43/44] FIX allow hr_payroll_attendance_holidays missing lines --- hr_payroll_attendance_holidays/hr_payslip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hr_payroll_attendance_holidays/hr_payslip.py b/hr_payroll_attendance_holidays/hr_payslip.py index 542e791d..f60f0c22 100755 --- a/hr_payroll_attendance_holidays/hr_payslip.py +++ b/hr_payroll_attendance_holidays/hr_payslip.py @@ -25,7 +25,7 @@ class HrPayslip(models.Model): 'contract_id': contract.id, } - res = super(HrPayslip, self).get_worked_day_lines(contracts.filtered(lambda c: not c.paid_hourly_attendance), date_from, date_to) + res = super(HrPayslip, self).get_worked_day_lines(contracts, date_from, date_to) res.extend(leaves.values()) return res From 5f238825b661980a249236c07f8d86c0030ae474 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 30 Jun 2018 13:30:33 -0700 Subject: [PATCH 44/44] Add external OCA/server-tools (for `dbfilter_from_header`) --- .gitmodules | 4 ++++ dbfilter_from_header | 1 + external/hibou-oca/server-tools | 1 + 3 files changed, 6 insertions(+) create mode 120000 dbfilter_from_header create mode 160000 external/hibou-oca/server-tools diff --git a/.gitmodules b/.gitmodules index e0ee1821..9ef0222b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,7 @@ [submodule "external/hibou-oca/account-analytic"] path = external/hibou-oca/account-analytic url = https://github.com/hibou-io/oca-account-analytic.git + +[submodule "external/hibou-oca/server-tools"] + path = external/hibou-oca/server-tools + url = https://github.com/hibou-io/oca-server-tools.git diff --git a/dbfilter_from_header b/dbfilter_from_header new file mode 120000 index 00000000..20c6d4c9 --- /dev/null +++ b/dbfilter_from_header @@ -0,0 +1 @@ +external/hibou-oca/server-tools/dbfilter_from_header \ No newline at end of file diff --git a/external/hibou-oca/server-tools b/external/hibou-oca/server-tools new file mode 160000 index 00000000..4bd10c59 --- /dev/null +++ b/external/hibou-oca/server-tools @@ -0,0 +1 @@ +Subproject commit 4bd10c59461bca2a0b8afd9028ac888b53cc5eef