From 2fdfc7f9168ed17f189363f3d6b5c92a8d4cbb13 Mon Sep 17 00:00:00 2001 From: Jared Kipe Date: Sat, 16 Mar 2019 10:43:43 -0700 Subject: [PATCH] MIG `sale_planner` to 12.0 --- sale_planner/__manifest__.py | 4 +- sale_planner/models/__init__.py | 9 +++-- sale_planner/models/resource.py | 47 +++++++++++++++++++++++ sale_planner/security/ir.model.access.csv | 2 + sale_planner/tests/test_planner.py | 24 +++++++++--- sale_planner/wizard/order_planner.py | 36 ++++++----------- 6 files changed, 85 insertions(+), 37 deletions(-) create mode 100644 sale_planner/models/resource.py create mode 100644 sale_planner/security/ir.model.access.csv diff --git a/sale_planner/__manifest__.py b/sale_planner/__manifest__.py index bdb3d28c..680a4538 100644 --- a/sale_planner/__manifest__.py +++ b/sale_planner/__manifest__.py @@ -1,7 +1,7 @@ { 'name': 'Sale Order Planner', 'summary': 'Plans order dates and warehouses.', - 'version': '11.0.1.0.0', + 'version': '12.0.1.0.0', 'author': "Hibou Corp.", 'category': 'Sale', 'license': 'AGPL-3', @@ -23,7 +23,6 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?) """, 'depends': [ - 'sale_order_dates', 'sale_sourced_by_line', 'base_geolocalize', 'delivery', @@ -32,6 +31,7 @@ on the specific method's characteristics. (e.g. Do they deliver on Saturday?) ], 'demo': [], 'data': [ + 'security/ir.model.access.csv', 'wizard/order_planner_views.xml', 'views/sale.xml', 'views/stock.xml', diff --git a/sale_planner/models/__init__.py b/sale_planner/models/__init__.py index 4257587e..ff524f40 100644 --- a/sale_planner/models/__init__.py +++ b/sale_planner/models/__init__.py @@ -1,6 +1,7 @@ +from . import delivery +from . import partner +from . import planning +from . import product +from . import resource from . import sale from . import stock -from . import delivery -from . import product -from . import planning -from . import partner diff --git a/sale_planner/models/resource.py b/sale_planner/models/resource.py new file mode 100644 index 00000000..0f4e7b43 --- /dev/null +++ b/sale_planner/models/resource.py @@ -0,0 +1,47 @@ +from functools import partial +from datetime import timedelta +from odoo import api, models +from odoo.addons.resource.models.resource import make_aware + + +class ResourceCalendar(models.Model): + _inherit = 'resource.calendar' + + @api.multi + def plan_days_end(self, days, day_dt, compute_leaves=False, domain=None): + """ + Override to `plan_days` that allows you to get the nearest 'end' including today. + """ + day_dt, revert = make_aware(day_dt) + + # which method to use for retrieving intervals + if compute_leaves: + get_intervals = partial(self._work_intervals, domain=domain) + else: + get_intervals = self._attendance_intervals + + if days >= 0: + found = set() + delta = timedelta(days=14) + for n in range(100): + dt = day_dt + delta * n + for start, stop, meta in get_intervals(dt, dt + delta): + found.add(start.date()) + if len(found) >= days: + return revert(stop) + return False + + elif days < 0: + days = abs(days) + found = set() + delta = timedelta(days=14) + for n in range(100): + dt = day_dt - delta * n + for start, stop, meta in reversed(get_intervals(dt - delta, dt)): + found.add(start.date()) + if len(found) == days: + return revert(start) + return False + + else: + return revert(day_dt) diff --git a/sale_planner/security/ir.model.access.csv b/sale_planner/security/ir.model.access.csv new file mode 100644 index 00000000..76f4df22 --- /dev/null +++ b/sale_planner/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_sale_order_planning_policy,access_sale_order_planning_policy,model_sale_order_planning_policy,base.group_user,1,1,1,1 \ No newline at end of file diff --git a/sale_planner/tests/test_planner.py b/sale_planner/tests/test_planner.py index 35dad49c..9f5e3f16 100644 --- a/sale_planner/tests/test_planner.py +++ b/sale_planner/tests/test_planner.py @@ -48,26 +48,38 @@ class TestPlanner(common.TransactionCase): 'partner_latitude': 40.51525, 'partner_longitude': -107.54645, }) + hour_from = (self.today.hour - 1) % 24 + hour_to = (self.today.hour + 1) % 24 + if hour_to < hour_from: + hour_to, hour_from = hour_from, hour_to + self.warehouse_calendar_1 = self.env['resource.calendar'].create({ 'name': 'Washington Warehouse Hours', + 'tz': 'UTC', '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}), + 'hour_from': hour_from, + 'hour_to': hour_to, + 'day_period': 'morning'}), + (0, 0, {'name': 'tomorrow', 'dayofweek': str(self.tomorrow.weekday()), - 'hour_from': (self.tomorrow.hour - 1) % 24, - 'hour_to': (self.tomorrow.hour + 1) % 24}), + 'hour_from': hour_from, + 'hour_to': hour_to, + 'day_period': 'morning'}), + ] }) self.warehouse_calendar_2 = self.env['resource.calendar'].create({ 'name': 'Colorado Warehouse Hours', + 'tz': 'UTC', '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}), + 'hour_from': hour_from, + 'hour_to': hour_to, + 'day_period': 'morning'}), ] }) self.warehouse_1 = self.env['stock.warehouse'].create({ diff --git a/sale_planner/wizard/order_planner.py b/sale_planner/wizard/order_planner.py index 75c81fca..fd3c7445 100644 --- a/sale_planner/wizard/order_planner.py +++ b/sale_planner/wizard/order_planner.py @@ -1,7 +1,7 @@ from math import sin, cos, sqrt, atan2, radians from json import dumps, loads from copy import deepcopy -from datetime import datetime +from datetime import datetime, timedelta from collections import defaultdict from logging import getLogger @@ -299,7 +299,6 @@ class SaleOrderMakePlan(models.TransientModel): return Carrier.search(domain) def _generate_base_option(self, order_fake, policy_group): - policy = False flag_force_closest = False warehouse_domain = False if 'policy' in policy_group: @@ -382,11 +381,11 @@ class SaleOrderMakePlan(models.TransientModel): # nobody has stock! primary_wh = self._find_closest_warehouse_by_partner(warehouses, order_fake.partner_shipping_id) - #order_fake.warehouse_id = primary_wh return {'warehouse_id': primary_wh.id} def generate_base_option(self, order_fake): _logger.error('generate_base_option:') + __start_date = datetime.now() - timedelta(days=-30) product_lines = list(filter(lambda line: line.product_id.type == 'product', order_fake.order_line)) if not product_lines: return {} @@ -410,7 +409,6 @@ class SaleOrderMakePlan(models.TransientModel): policy_groups[0]['products'].append(p) policy_groups[0]['buy_qty'][p.id] = buy_qty[p.id] - for _, policy_group in policy_groups.items(): product_set = self.env['product.product'].browse() for p in policy_group['products']: @@ -418,8 +416,7 @@ class SaleOrderMakePlan(models.TransientModel): policy_group['products'] = product_set policy_group['base_option'] = self._generate_base_option(order_fake, policy_group) - - option_policy_groups = defaultdict(lambda: {'products': self.env['product.product'].browse(), 'policies': self.env['sale.order.planning.policy'].browse(), 'date_planned': '1900', 'sub_options': [],}) + option_policy_groups = defaultdict(lambda: {'products': self.env['product.product'].browse(), 'policies': self.env['sale.order.planning.policy'].browse(), 'date_planned': __start_date, 'sub_options': [],}) for policy_id, policy_group in policy_groups.items(): base_option = policy_group['base_option'] _logger.error(' base_option: ' + str(base_option)) @@ -444,7 +441,7 @@ class SaleOrderMakePlan(models.TransientModel): if not option_group['sub_options']: del option_group['sub_options'] else: - sub_options = defaultdict(lambda: {'date_planned': '1900', 'product_ids': [], 'product_skus': []}) + sub_options = defaultdict(lambda: {'date_planned': __start_date, 'product_ids': [], 'product_skus': []}) remaining_products = option_group['products'] for options in option_group['sub_options']: for wh_id, option in options.items(): @@ -466,7 +463,7 @@ class SaleOrderMakePlan(models.TransientModel): # At this point we should have all of the policy options collapsed. # Collapse warehouse options. - base_option = {'date_planned': '1900', 'products': self.env['product.product'].browse()} + base_option = {'date_planned': __start_date, 'products': self.env['product.product'].browse()} for wh_id, intermediate_option in option_policy_groups.items(): _logger.error(' base_option: ' + str(base_option)) _logger.error(' intermediate_option: ' + str(intermediate_option)) @@ -596,7 +593,7 @@ class SaleOrderMakePlan(models.TransientModel): def _next_warehouse_shipping_date(self, warehouse): if warehouse.shipping_calendar_id: - return fields.Datetime.to_string(warehouse.shipping_calendar_id.plan_days(0.01, fields.Datetime.from_string(fields.Datetime.now()), compute_leaves=True)) + return warehouse.shipping_calendar_id.plan_days_end(0, fields.Datetime.now(), compute_leaves=True) return False @api.model @@ -683,15 +680,11 @@ class SaleOrderMakePlan(models.TransientModel): max_requested_date = None for option in sub_options.values(): requested_date = option.get('requested_date') - if requested_date and isinstance(requested_date, str): - requested_date = fields.Datetime.from_string(requested_date) if requested_date and not max_requested_date: max_requested_date = requested_date elif requested_date: if requested_date > max_requested_date: max_requested_date = requested_date - if max_requested_date: - return fields.Datetime.to_string(max_requested_date) return max_requested_date def _get_max_transit_days(self, sub_options): @@ -748,7 +741,7 @@ class SaleOrderMakePlan(models.TransientModel): option = deepcopy(base_option) option['carrier_id'] = carrier.id option['shipping_price'] = final_price - option['requested_date'] = fields.Datetime.to_string(date_delivered) if (date_delivered and isinstance(date_delivered, datetime)) else date_delivered + option['requested_date'] = date_delivered option['transit_days'] = transit_days return option except Exception as e: @@ -759,13 +752,15 @@ class SaleOrderMakePlan(models.TransientModel): return None - class SaleOrderPlanningOption(models.TransientModel): _name = 'sale.order.planning.option' _description = 'Order Planning Option' def create(self, values): if 'sub_options' in values and not isinstance(values['sub_options'], str): + for wh_id, option in values['sub_options'].items(): + if option.get('date_planned'): + option['date_planned'] = str(option['date_planned']) values['sub_options'] = dumps(values['sub_options']) return super(SaleOrderPlanningOption, self).create(values) @@ -782,13 +777,9 @@ class SaleOrderPlanningOption(models.TransientModel): line = '' for wh_id, wh_option in sub_options.items(): product_skus = wh_option.get('product_skus', []) + date_planned = wh_option.get('date_planned') product_skus = ', '.join(product_skus) requested_date = wh_option.get('requested_date', '') - if requested_date: - requested_date = self._context_datetime(requested_date) - date_planned = wh_option.get('date_planned', '') - if date_planned: - date_planned = self._context_datetime(date_planned) shipping_price = float(wh_option.get('shipping_price', 0.0)) transit_days = int(wh_option.get('transit_days', 0)) @@ -817,8 +808,3 @@ class SaleOrderPlanningOption(models.TransientModel): for option in self: option.plan_id.select_option(option) return - - def _context_datetime(self, date): - date = fields.Datetime.from_string(date) - date = fields.Datetime.context_timestamp(self, date) - return fields.Datetime.to_string(date)