Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[ADD] [16.0] partner_resource_delivery_schedule #747

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
[ADD] partner_resource_delivery_schedule
Shide committed Feb 1, 2024
commit 42e562ff4e060b9c3ab6d3e4b78acb785251b53a
152 changes: 152 additions & 0 deletions partner_resource_delivery_schedule/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
==================================
Partner Resource Delivery Schedule
==================================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:2a6b2828fef140492cd8486f44d2496b3c25abdf758f6377e025fa9b68b2d4bd
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |badge2| image:: https://img.shields.io/badge/licence-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fdelivery--carrier-lightgray.png?logo=github
:target: https://github.com/OCA/delivery-carrier/tree/16.0/partner_resource_delivery_schedule
:alt: OCA/delivery-carrier
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/delivery-carrier-16-0/delivery-carrier-16-0-partner_resource_delivery_schedule
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/delivery-carrier&target_branch=16.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module extends the functionality of sales to support contact
availability windows and to allow you to plan better the expected date
on sales orders and pickings.

Only Sales Administrators can create *new* Delivery Schedules.

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Use Cases / Context
===================

This module was developed because there is no way to define availability
windows on contacts.

It will be useful for you if you need to have a better expected date on
your sales and pickings.

If you need this module for those reasons, these might interest you too:

- ``delivery_carrier_resource_schedule``

Usage
=====

To use this module, you need to:

*Configuration:*

1. Configure ``partner_resource_delivery_schedule.interval_days``
parameter and set the days (integer) for the window you want to
consider to find an available schedule. If parameter doesn't exist,
60 days will be used.

2. Create a new Delivery Schedule on Sales > Configuration > Delivery
Schedules menu.

*Sales:*

1. Go to a Customer
2. Click on Enable Delivery Schedule under Sales page
3. Change the Delivery Schedule Calendar or create a new one (Sales
Administrators)
4. Create a Sales Order for this Customer with a non Service product
with Quantity > 0
5. Click on the button (>>) close to the commitment date (do it several
times) and check is calculated having in consideration the
Availability of this Customer resource and Company calendar

*Stock:*

6. On the picking of the previous sale, modify quantity done on the
picking in order to allow backorder creation
7. Modify the Scheduled date prior today in order to recompute scheduled
date of the new backorder
8. Create a Backorder
9. Check the Backorder schedule date is calculated having in
consideration the Availability of the Customer resource and Company
calendar

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/OCA/delivery-carrier/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/delivery-carrier/issues/new?body=module:%20partner_resource_delivery_schedule%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

Do not contact contributors directly about support or help with technical issues.

Credits
=======

Authors
-------

* Moduon

Contributors
------------

- Eduardo de Miguel (`Moduon <https://www.moduon.team/>`__)
- Emilio Pascual (`Moduon <https://www.moduon.team/>`__)
- Rafael Blasco (`Moduon <https://www.moduon.team/>`__)

Maintainers
-----------

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

.. |maintainer-Shide| image:: https://github.com/Shide.png?size=40px
:target: https://github.com/Shide
:alt: Shide
.. |maintainer-EmilioPascual| image:: https://github.com/EmilioPascual.png?size=40px
:target: https://github.com/EmilioPascual
:alt: EmilioPascual
.. |maintainer-yajo| image:: https://github.com/yajo.png?size=40px
:target: https://github.com/yajo
:alt: yajo

Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:

|maintainer-Shide| |maintainer-EmilioPascual| |maintainer-yajo|

This module is part of the `OCA/delivery-carrier <https://github.com/OCA/delivery-carrier/tree/16.0/partner_resource_delivery_schedule>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions partner_resource_delivery_schedule/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
28 changes: 28 additions & 0 deletions partner_resource_delivery_schedule/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

{
"name": "Partner Resource Delivery Schedule",
"summary": "Allow to set Availability windows on Partners",
"version": "16.0.1.0.0",
"development_status": "Alpha",
"category": "Sales",
"website": "https://github.com/OCA/delivery-carrier",
"author": "Moduon, Odoo Community Association (OCA)",
"maintainers": ["Shide", "EmilioPascual", "yajo"],
"license": "LGPL-3",
"installable": True,
"external_dependencies": {"python": ["freezegun"]},
"depends": [
"resource",
"sale_stock",
],
"data": [
"security/ir.model.access.csv",
"security/delivery_schedule_security.xml",
"views/resource_resource_view.xml",
"views/resource_calendar_view.xml",
"views/res_partner_view.xml",
"views/sale_order_view.xml",
],
}
122 changes: 122 additions & 0 deletions partner_resource_delivery_schedule/i18n/es.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * partner_resource_delivery_schedule
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-01 11:32+0000\n"
"PO-Revision-Date: 2024-02-01 12:34+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.2\n"

#. module: partner_resource_delivery_schedule
#. odoo-python
#: code:addons/partner_resource_delivery_schedule/models/sale_order.py:0
#, python-format
msgid ""
"Commitment date or expected date is required to calculate the next "
"commitment date"
msgstr ""
"La Fecha de Entrega o la Fecha Esperada son requeridas para calcular la "
"siguiente Fecha de Entrega"

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_res_partner
msgid "Contact"
msgstr "Contacto"

#. module: partner_resource_delivery_schedule
#: model_terms:ir.actions.act_window,help:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_action
msgid "Create a new Delivery Schedule record"
msgstr "Crear un nuevo registro de Planificación de Entrega"

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields.selection,name:partner_resource_delivery_schedule.selection__resource_resource__resource_type__delivery
msgid "Delivery"
msgstr "Entrega"

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_partner__delivery_schedule_calendar_id
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_users__delivery_schedule_calendar_id
msgid "Delivery Schedule Calendar"
msgstr "Calendario de planificación de entregas"

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_partner__delivery_schedule_resource_id
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_users__delivery_schedule_resource_id
msgid "Delivery Schedule Resource"
msgstr "Recurso de planificación de entregas"

#. module: partner_resource_delivery_schedule
#: model:ir.actions.act_window,name:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_action
#: model:ir.ui.menu,name:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_sales_menu
msgid "Delivery Schedules"
msgstr "Planificaciones de entregas"

#. module: partner_resource_delivery_schedule
#: model_terms:ir.ui.view,arch_db:partner_resource_delivery_schedule.view_partner_form
msgid "Enable Delivery Schedule"
msgstr "Habilitar planificación de entregas"

#. module: partner_resource_delivery_schedule
#: model_terms:ir.ui.view,arch_db:partner_resource_delivery_schedule.view_order_form
msgid "Find next available date"
msgstr "Buscar la siguiente fecha disponible"

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_resource_calendar__is_delivery_schedule
msgid "Is Delivery Schedule"
msgstr "Es una planificación de entrega"

#. module: partner_resource_delivery_schedule
#. odoo-python
#: code:addons/partner_resource_delivery_schedule/models/sale_order.py:0
#, python-format
msgid ""
"No available date found for commitment date in the next %(interval_days)s "
"days since %(date_start)s."
msgstr ""
"No se ha encontrado ninguna fecha para la Fecha de Entrega en los "
"próximos %(interval_days)s días desde %(date_start)s."

#. module: partner_resource_delivery_schedule
#: model_terms:ir.actions.act_window,help:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_action
msgid ""
"Once the Delivery Schedule record is created, you can assign it to a "
"Customer."
msgstr ""
"Una vez que el registro de Planificación de entrega está creado, puedes "
"asignarlo a un Cliente."

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_resource_calendar
msgid "Resource Working Time"
msgstr "Tiempo de Trabajo de Recursos"

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_resource_resource
msgid "Resources"
msgstr "Recursos"

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_sale_order
msgid "Sales Order"
msgstr "Pedido de venta"

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_stock_picking
msgid "Transfer"
msgstr "Albarán"

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_resource_resource__resource_type
msgid "Type"
msgstr "Tipo"
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * partner_resource_delivery_schedule
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-01 11:31+0000\n"
"PO-Revision-Date: 2024-02-01 11:31+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"

#. module: partner_resource_delivery_schedule
#. odoo-python
#: code:addons/partner_resource_delivery_schedule/models/sale_order.py:0
#, python-format
msgid ""
"Commitment date or expected date is required to calculate the next "
"commitment date"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_res_partner
msgid "Contact"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model_terms:ir.actions.act_window,help:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_action
msgid "Create a new Delivery Schedule record"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields.selection,name:partner_resource_delivery_schedule.selection__resource_resource__resource_type__delivery
msgid "Delivery"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_partner__delivery_schedule_calendar_id
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_users__delivery_schedule_calendar_id
msgid "Delivery Schedule Calendar"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_partner__delivery_schedule_resource_id
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_res_users__delivery_schedule_resource_id
msgid "Delivery Schedule Resource"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.actions.act_window,name:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_action
#: model:ir.ui.menu,name:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_sales_menu
msgid "Delivery Schedules"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model_terms:ir.ui.view,arch_db:partner_resource_delivery_schedule.view_partner_form
msgid "Enable Delivery Schedule"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model_terms:ir.ui.view,arch_db:partner_resource_delivery_schedule.view_order_form
msgid "Find next available date"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_resource_calendar__is_delivery_schedule
msgid "Is Delivery Schedule"
msgstr ""

#. module: partner_resource_delivery_schedule
#. odoo-python
#: code:addons/partner_resource_delivery_schedule/models/sale_order.py:0
#, python-format
msgid ""
"No available date found for commitment date in the next %(interval_days)s "
"days since %(date_start)s."
msgstr ""

#. module: partner_resource_delivery_schedule
#: model_terms:ir.actions.act_window,help:partner_resource_delivery_schedule.resource_calendar_delivery_schedule_action
msgid ""
"Once the Delivery Schedule record is created, you can assign it to a "
"Customer."
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_resource_calendar
msgid "Resource Working Time"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_resource_resource
msgid "Resources"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_sale_order
msgid "Sales Order"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model,name:partner_resource_delivery_schedule.model_stock_picking
msgid "Transfer"
msgstr ""

#. module: partner_resource_delivery_schedule
#: model:ir.model.fields,field_description:partner_resource_delivery_schedule.field_resource_resource__resource_type
msgid "Type"
msgstr ""
6 changes: 6 additions & 0 deletions partner_resource_delivery_schedule/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from . import helpers
from . import resource_resource
from . import resource_calendar
from . import res_partner
from . import sale_order
from . import stock_picking
63 changes: 63 additions & 0 deletions partner_resource_delivery_schedule/models/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from datetime import datetime

from odoo import fields, models

from odoo.addons.resource.models.resource import datetime_to_string, make_aware


def get_next_available_date(
date_from: datetime,
date_to: datetime,
resources: models.Model = None,
calendars: models.Model = None,
) -> datetime:
"""Get next available date of the given resource between the given dates
and substracting the unavailable calendars and resources
:param date_from: Date from which to start searching
:type date_from: datetime.datetime
:param date_to: Date to which to stop searching
:type date_to: datetime.datetime
:param resources: Resources to match the next available date
:type resources: resource.resource
:param calendars: Calendars to match the next available date
:type calendars: resource.calendar
:return: Next available date
:rtype: datetime.datetime or None
"""
if not resources and not calendars:
return

Check warning on line 32 in partner_resource_delivery_schedule/models/helpers.py

Codecov / codecov/patch

partner_resource_delivery_schedule/models/helpers.py#L32

Added line #L32 was not covered by tests

aware_date_from, dummy = make_aware(date_from)
aware_date_to, dummy = make_aware(date_to)

intervals_list = []
if resources:
resource_intervals, _cwi = resources._get_valid_work_intervals(
aware_date_from,
aware_date_to,
)
intervals_list.extend(list(resource_intervals.values()))

if calendars:
for calendar in calendars:
calendar_intervals = calendar._work_intervals_batch(
aware_date_from,
aware_date_to,
compute_leaves=True,
)
for cal_interval in calendar_intervals.values():
intervals_list.append(cal_interval)

if not intervals_list:
return

Check warning on line 56 in partner_resource_delivery_schedule/models/helpers.py

Codecov / codecov/patch

partner_resource_delivery_schedule/models/helpers.py#L56

Added line #L56 was not covered by tests

availability_interval = intervals_list[0]
for intervals in intervals_list[1:]:
availability_interval = availability_interval & intervals

for ds, _de, _r in availability_interval:
return fields.Datetime.from_string(datetime_to_string(ds))
64 changes: 64 additions & 0 deletions partner_resource_delivery_schedule/models/res_partner.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from odoo import _, api, fields, models


class ResPartner(models.Model):
_inherit = "res.partner"

delivery_schedule_resource_id = fields.Many2one(
comodel_name="resource.resource",
string="Delivery Schedule Resource",
domain=[("resource_type", "=", "delivery")],
readonly=True,
)
delivery_schedule_calendar_id = fields.Many2one(
comodel_name="resource.calendar",
string="Delivery Schedule Calendar",
compute="_compute_delivery_schedule_calendar_id",
inverse="_inverse_delivery_schedule_calendar_id",
domain=[("is_delivery_schedule", "=", True)],
compute_sudo=True,
store=True,
tracking=True,
)

def action_create_delivery_schedule_resource(self):
"""Create a delivery schedule resource for the partner."""
for record in self:
if record.delivery_schedule_resource_id:
continue

Check warning on line 31 in partner_resource_delivery_schedule/models/res_partner.py

Codecov / codecov/patch

partner_resource_delivery_schedule/models/res_partner.py#L31

Added line #L31 was not covered by tests
record.delivery_schedule_resource_id = (
self.env["resource.resource"]
.sudo()
.create(
{
"name": f"[{_('Delivery Schedule')}] {record.display_name}",
"resource_type": "delivery",
"company_id": record.company_id.id,
}
)
)

@api.depends("delivery_schedule_resource_id.calendar_id")
def _compute_delivery_schedule_calendar_id(self):
"""Get the calendar associated to the schedule resource"""
for record in self:
record.delivery_schedule_calendar_id = (
record.delivery_schedule_resource_id.calendar_id
)

def _inverse_delivery_schedule_calendar_id(self):
"""Set the calendar on the associated schedule resource or remove the resource
if no calendar is set. Resources must have an associated calendar."""
for record in self:
if not record.delivery_schedule_calendar_id:
record.delivery_schedule_resource_id.sudo().unlink()
continue
# Resource exists > update calendar
if not record.delivery_schedule_resource_id:
record.action_create_delivery_schedule_resource()

Check warning on line 61 in partner_resource_delivery_schedule/models/res_partner.py

Codecov / codecov/patch

partner_resource_delivery_schedule/models/res_partner.py#L61

Added line #L61 was not covered by tests
record.sudo().delivery_schedule_resource_id.calendar_id = (
record.delivery_schedule_calendar_id
)
19 changes: 19 additions & 0 deletions partner_resource_delivery_schedule/models/resource_calendar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from odoo import api, fields, models


class ResourceCalendar(models.Model):
_inherit = "resource.calendar"

is_delivery_schedule = fields.Boolean()

@api.model
def check_access_rights(self, operation, raise_exception=True):
res = super().check_access_rights(operation, raise_exception=raise_exception)
return res

def check_access_rule(self, operation):
res = super().check_access_rule(operation)
return res
13 changes: 13 additions & 0 deletions partner_resource_delivery_schedule/models/resource_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from odoo import fields, models


class ResourceResource(models.Model):
_inherit = "resource.resource"

resource_type = fields.Selection(
selection_add=[("delivery", "Delivery")],
ondelete={"delivery": "set default"},
)
78 changes: 78 additions & 0 deletions partner_resource_delivery_schedule/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from datetime import datetime, timedelta

from odoo import _, exceptions, fields, models

from .helpers import get_next_available_date


class SaleOrder(models.Model):
_inherit = "sale.order"

def _get_commitment_date_resources_and_calendars(self):
"""Resources and Calendars for taking in consderation
for commitment date calculation of an order."""
resources = self.env["resource.resource"].browse()
calendars = self.mapped("company_id.resource_calendar_id")
sr_partner = (self.partner_shipping_id or self.partner_id).filtered(
"delivery_schedule_resource_id"
)
if sr_partner:
resources = sr_partner.mapped("delivery_schedule_resource_id")
calendars |= sr_partner.mapped("delivery_schedule_calendar_id")
return resources, calendars

def calc_next_commitment_date(self):
"""Calculate the next commitment date for the order."""
self.ensure_one()
interval_days = int(
self.env["ir.config_parameter"]
.sudo()
.get_param("partner_resource_delivery_schedule.interval_days", 60)
)

# Choose date_start
if self.commitment_date:
date_start = self.commitment_date
elif self.expected_date:
date_start = self.expected_date - timedelta(days=1)
# Increased after when choosing next day (will match expected_date)
else:
# Raise Exception because we would expect at least expected_date
raise exceptions.UserError(

Check warning on line 44 in partner_resource_delivery_schedule/models/sale_order.py

Codecov / codecov/patch

partner_resource_delivery_schedule/models/sale_order.py#L44

Added line #L44 was not covered by tests
_(
"Commitment date or expected date "
"is required to calculate the next commitment date"
)
)
# Set now() if date_start is in the past
if date_start.date() <= fields.date.today():
date_start = fields.datetime.now()
# Choose next day after date_start
date_start = datetime.combine(
date_start.date(), datetime.min.time()
) + timedelta(days=1)

# Get next available date
(
resources,
calendars,
) = self._get_commitment_date_resources_and_calendars()
next_available_date = get_next_available_date(
date_start,
date_start + timedelta(days=interval_days),
resources=resources,
calendars=calendars,
)
if not next_available_date:
raise exceptions.UserError(

Check warning on line 70 in partner_resource_delivery_schedule/models/sale_order.py

Codecov / codecov/patch

partner_resource_delivery_schedule/models/sale_order.py#L70

Added line #L70 was not covered by tests
_(
"No available date found for commitment date "
"in the next %(interval_days)s days since %(date_start)s.",
interval_days=interval_days,
date_start=date_start,
)
)
self.commitment_date = next_available_date
52 changes: 52 additions & 0 deletions partner_resource_delivery_schedule/models/stock_picking.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from datetime import datetime, timedelta

from odoo import fields, models

from .helpers import get_next_available_date


class StockPicking(models.Model):
_inherit = "stock.picking"

def _get_commitment_date_resources_and_calendars(self):
"""Resources and Calendars for taking in consderation
for commitment date calculation of a picking."""
resources = self.env["resource.resource"].browse()
calendars = self.mapped("company_id.resource_calendar_id")
sr_partner = self.partner_id.filtered("delivery_schedule_resource_id")
if sr_partner:
resources = sr_partner.mapped("delivery_schedule_resource_id")
calendars |= sr_partner.mapped("delivery_schedule_calendar_id")
return resources, calendars

def _create_backorder(self):
"""When a backorder is created, recompute commitment date for the new
picking if scheduled_date it's in the past."""
backorders = super()._create_backorder()
interval_days = int(
self.env["ir.config_parameter"]
.sudo()
.get_param("partner_resource_delivery_schedule.interval_days", 60)
)
for backorder in backorders:
if fields.Date.today() >= backorder.scheduled_date.date():
tomorrow = datetime.combine(
fields.Date.today(), datetime.min.time()
) + timedelta(days=1)
# Calc commitment date for the new picking
(
resources,
calendars,
) = backorder._get_commitment_date_resources_and_calendars()
next_available_date = get_next_available_date(
tomorrow,
tomorrow + timedelta(days=interval_days),
resources=resources,
calendars=calendars,
)
# Set tomorrow if no date found because maybe tomorrow can send it
backorder.scheduled_date = next_available_date or tomorrow
return backorders
9 changes: 9 additions & 0 deletions partner_resource_delivery_schedule/readme/CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
This module was developed because there is no way to define availability windows on
contacts.

It will be useful for you if you need to have a better expected date on your sales and
pickings.

If you need this module for those reasons, these might interest you too:

- `delivery_carrier_resource_schedule`
3 changes: 3 additions & 0 deletions partner_resource_delivery_schedule/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Eduardo de Miguel ([Moduon](https://www.moduon.team/))
- Emilio Pascual ([Moduon](https://www.moduon.team/))
- Rafael Blasco ([Moduon](https://www.moduon.team/))
4 changes: 4 additions & 0 deletions partner_resource_delivery_schedule/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
This module extends the functionality of sales to support contact availability windows
and to allow you to plan better the expected date on sales orders and pickings.

Only Sales Administrators can create *new* Delivery Schedules.
31 changes: 31 additions & 0 deletions partner_resource_delivery_schedule/readme/USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
To use this module, you need to:

*Configuration:*

1. Configure `partner_resource_delivery_schedule.interval_days` parameter and set
the days (integer) for the window you want to consider to find an available schedule.
If parameter doesn't exist, 60 days will be used.

2. Create a new Delivery Schedule on Sales > Configuration > Delivery Schedules menu.


*Sales:*

1. Go to a Customer
2. Click on Enable Delivery Schedule under Sales page
3. Change the Delivery Schedule Calendar or create a new one (Sales Administrators)
4. Create a Sales Order for this Customer with a non Service product with Quantity > 0
5. Click on the button (>>) close to the commitment date (do it several times) and
check is calculated having in consideration the Availability of this
Customer resource and Company calendar


*Stock:*

6. On the picking of the previous sale, modify quantity done on the picking in order to
allow backorder creation
7. Modify the Scheduled date prior today in order to recompute scheduled date of the new
backorder
8. Create a Backorder
9. Check the Backorder schedule date is calculated having in consideration the
Availability of the Customer resource and Company calendar
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="resource_calendar_delivery_schedule_sales_manager" model="ir.rule">
<field name="name">Manage Delivery Schedule Calendar - Sale Manager</field>
<field name="model_id" ref="resource.model_resource_calendar" />
<field name="perm_read" eval="False" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field
name="domain_force"
>[('is_delivery_schedule', '=', True), '|',('company_id','=',False),('company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]" />
</record>
<record
id="resource_calendar_attendance_delivery_schedule_sales_manager"
model="ir.rule"
>
<field
name="name"
>Manage Delivery Schedule Calendar Attendance - Sale Manager</field>
<field name="model_id" ref="resource.model_resource_calendar_attendance" />
<field name="perm_read" eval="False" />
<field name="perm_write" eval="True" />
<field name="perm_create" eval="True" />
<field name="perm_unlink" eval="True" />
<field
name="domain_force"
>[('calendar_id.is_delivery_schedule', '=', True), '|',('calendar_id.company_id','=',False),('calendar_id.company_id', 'in', company_ids)]</field>
<field name="groups" eval="[(4, ref('sales_team.group_sale_manager'))]" />
</record>
</odoo>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_delivery_schedule_resource_calendar_manager,delivery.schedule.resource.calendar.manager,resource.model_resource_calendar,sales_team.group_sale_manager,1,1,1,1
access_delivery_schedule_resource_calendar_attendance_manager,delivery.schedule.resource.calendar.attendance.manager,resource.model_resource_calendar_attendance,sales_team.group_sale_manager,1,1,1,1
482 changes: 482 additions & 0 deletions partner_resource_delivery_schedule/static/description/index.html

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions partner_resource_delivery_schedule/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import test_partner_expected_date
167 changes: 167 additions & 0 deletions partner_resource_delivery_schedule/tests/test_partner_expected_date.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Copyright 2023 Moduon Team S.L.
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0)

from datetime import datetime

from freezegun import freeze_time

from odoo.tests import tagged
from odoo.tests.common import TransactionCase
from odoo.tools import DEFAULT_SERVER_DATETIME_FORMAT


@freeze_time("2023-12-18")
@tagged("post_install", "-at_install")
class TestPartnerExpectedDate(TransactionCase):
def setUp(self):
super(TestPartnerExpectedDate, self).setUp()
# 2 weeks is enough for testing
self.env["ir.config_parameter"].sudo().set_param(
"partner_resource_delivery_schedule.interval_days", "14"
)
partner_calendar = self.env["resource.calendar"].create(
{
"name": "Fridays from 5 to 19",
"tz": "UTC",
"company_id": self.env.company.id,
"attendance_ids": [
(
0,
0,
{
"name": "Friday morning",
"dayofweek": "4",
"hour_from": 5.0,
"hour_to": 19.0,
"day_period": "morning",
},
)
],
}
)
self.partner = self.env["res.partner"].create(
{
"name": "Partner with Resource",
"company_id": None,
}
)
# Check that resource is created when calendar is set and deleted when calendar is unset
self.partner.action_create_delivery_schedule_resource()
self.partner.write({"delivery_schedule_calendar_id": partner_calendar.id})
self.assertEqual(
self.partner.delivery_schedule_resource_id.calendar_id,
partner_calendar,
"Calendar not set on resource",
)
self.partner.write({"delivery_schedule_calendar_id": False})
self.assertFalse(
self.partner.delivery_schedule_resource_id, "Resource not deleted"
)
self.partner.action_create_delivery_schedule_resource()
self.partner.write({"delivery_schedule_calendar_id": partner_calendar.id})
self.env["resource.calendar.leaves"].create(
{
"name": f"{self.partner.name} leave",
"resource_id": self.partner.delivery_schedule_resource_id.id,
"date_from": "2023-12-22 02:00:00",
"date_to": "2023-12-23 02:00:00",
"company_id": self.env.company.id,
}
)
self.env.company.resource_calendar_id.write({"tz": "UTC"})
self.env.company.resource_calendar_id.attendance_ids.filtered_domain(
[
("dayofweek", "=", "4"),
("day_period", "=", "morning"),
]
).write(
{
"hour_from": 11.0,
"hour_to": 12.0,
}
)
self.product = self.env["product.product"].create(
{
"name": "Product",
"list_price": 5,
"type": "product",
"uom_id": self.env.ref("uom.product_uom_unit").id,
}
)
location = self.env["stock.location"].create(
{"name": "Test location", "usage": "internal"}
)
self.env["stock.quant"].create(
{
"quantity": 100,
"location_id": location.id,
"product_id": self.product.id,
}
)

def _create_partner_order(self):
return self.env["sale.order"].create(
{
"partner_id": self.partner.id,
"state": "draft",
"order_line": [
(
0,
0,
{
"name": self.product.name,
"product_id": self.product.id,
"product_uom": self.product.uom_id.id,
"price_unit": self.product.list_price,
"product_uom_qty": 10,
},
)
],
}
)

def test_partner_expected_date(self):
# Create order
order = self._create_partner_order()
# Get first available date
order.calc_next_commitment_date()
first_available_date = datetime.strptime(
"2023-12-29 11:00:00", DEFAULT_SERVER_DATETIME_FORMAT
)
self.assertEqual(order.commitment_date, first_available_date)
# Get next available date
order.calc_next_commitment_date()
next_available_date = datetime.strptime(
"2024-01-05 11:00:00", DEFAULT_SERVER_DATETIME_FORMAT
)
self.assertEqual(order.commitment_date, next_available_date)
# Create picking
order.action_confirm()
main_picking = order.picking_ids[0]
self.assertEqual(main_picking.scheduled_date, order.commitment_date)
self.assertEqual(main_picking.date_deadline, order.commitment_date)
# Prepare picking to create backorder
main_picking.move_ids.write({"quantity_done": 5})
# Prepare picking to trigger a new expected date on the backorder
main_picking.write({"scheduled_date": "2023-12-17"})
# Skip backorder wizard and set directly the pickings that needs backorder
main_picking.with_context(
button_validate_picking_ids=main_picking.ids, skip_backorder=True
).button_validate()
backorder_picking = order.picking_ids[-1]
# Because picking has been done after the scheduled date,
# recomputation of the expected date is the first available date
self.assertEqual(backorder_picking.scheduled_date, first_available_date)

def test_partner_commitment_date(self):
# Create order
order = self._create_partner_order()
commitment_date = datetime.strptime(
"2023-12-22 12:00:00", DEFAULT_SERVER_DATETIME_FORMAT
)
order.write({"commitment_date": commitment_date})
order.action_confirm()
main_picking = order.picking_ids[0]
# Ensure that commitment date is propagated to the picking
self.assertEqual(main_picking.scheduled_date, commitment_date)
self.assertEqual(main_picking.date_deadline, commitment_date)
32 changes: 32 additions & 0 deletions partner_resource_delivery_schedule/views/res_partner_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Moduon Team S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<data>
<record id="view_partner_form" model="ir.ui.view">
<field name="name">res.partner.form</field>
<field name="model">res.partner</field>
<field name="inherit_id" ref="base.view_partner_form" />
<field name="arch" type="xml">
<xpath expr="//group[@name='sale']" position="inside">
<field name="delivery_schedule_resource_id" invisible="1" />
<label for="delivery_schedule_calendar_id" />
<div class="o_row">
<button
name="action_create_delivery_schedule_resource"
string="Enable Delivery Schedule"
type="object"
icon="fa-calendar-check-o"
class="btn-secondary"
attrs="{'invisible': [('delivery_schedule_resource_id', '!=', False)]}"
/>
<field
name="delivery_schedule_calendar_id"
attrs="{'invisible': [('delivery_schedule_resource_id', '=', False)]}"
context="{'default_is_delivery_schedule': True}"
nolabel="1"
/>
</div>
</xpath>
</field>
</record>
</data>
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Moduon Team S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<data>
<record
id="resource_calendar_delivery_schedule_action"
model="ir.actions.act_window"
>
<field name="name">Delivery Schedules</field>
<field name="res_model">resource.calendar</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('is_delivery_schedule', '=', True)]</field>
<field name="context">{'default_is_delivery_schedule': True}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a new Delivery Schedule record
</p>
<p>
Once the Delivery Schedule record is created, you can assign it to a Customer.
</p>
</field>
</record>

<menuitem
id="resource_calendar_delivery_schedule_sales_menu"
action="resource_calendar_delivery_schedule_action"
parent="sale.menu_sales_config"
groups="sales_team.group_sale_manager"
sequence="6"
/>
</data>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Moduon Team S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<data>
<record id="resource_resource_form" model="ir.ui.view">
<field name="name">resource.resource.form</field>
<field name="model">resource.resource</field>
<field name="inherit_id" ref="resource.resource_resource_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='user_id']" position="attributes">
<attribute
name="attrs"
>{'required': [('resource_type', '=', 'user')], 'invisible': [('resource_type', '!=', 'user')]}</attribute>
</xpath>
</field>
</record>
</data>
22 changes: 22 additions & 0 deletions partner_resource_delivery_schedule/views/sale_order_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Copyright 2023 Moduon Team S.L.
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0) -->
<data>
<record id="view_order_form" model="ir.ui.view">
<field name="name">sale.order.form</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="arch" type="xml">
<xpath expr="//field[@name='commitment_date']" position="before">
<button
name="calc_next_commitment_date"
icon="fa-forward"
class="btn-link p-0"
string=""
type="object"
title="Find next available date"
/>
</xpath>
</field>
</record>
</data>
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# generated from manifests external_dependencies
freezegun
roulier
6 changes: 6 additions & 0 deletions setup/partner_resource_delivery_schedule/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import setuptools

setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)