Skip to content

Conversation

@nicolas-delbovier-acsone

This PR introduces several refactorings to improve module isolation and extend the Shopinvader API:

  1. sale_typology module creation: Isolates the sale order typology logic.
  2. sale_quotation extension: Updates sale_quotation to depend on the new sale_typology module.
  3. sale_cart_quotation module creation: Decouples sale_quotation from sale_cart.
  4. shopinvader_api_quotation enhancement: Enables line creation, update, and deletion within the quotation API.
  5. shopinvader_api_cart_quotation module extraction: Moves the cart-to-quotation transformation logic from shopinvader_api_quotation to its own dedicated module.

* Extracts the shop_order_mode of products in a new module
* Enables to choose a combined shop order mode (quotation or direct sale)
* Adds a migration script to translate the products and templates order mode infos from the old to the new fields
@nicolas-delbovier-acsone nicolas-delbovier-acsone changed the title 16.0 refactor quotations rest api [16.0] refactor quotations rest api May 21, 2025
@sebastienbeau sebastienbeau added this to the 16.0 milestone Jun 2, 2025
…nce API

- put shopinvader_sale_state as a dependency in order to remove sale_cart because we need the field sale.order.shopinvader_state

This commit introduces significant enhancements to the REST API for quotation management, including:

- **Create Method:** Added a new endpoint to create quotations.
- **Line Management Methods:** Implemented dedicated endpoints for adding, deleting, or updating single or multiple lines within a quotation.
- **Update Extension:** Extended the existing update method to support the addition of new lines
…tion" action in the form view of sale.order + refine module description
- Remove typology field from quotation create request
- Implement following logic in the update process:
  -If the line has an id -> update existing line (raise if not found)
  -If the line has no id -> create a new one
  -All existing lines not found in lines should be removed
- Small refactoring to avoid code dup
- Test invalid line ids in unit tests
@nicolas-delbovier-acsone nicolas-delbovier-acsone force-pushed the 16.0-refactor-quotations-rest-api branch from e676a2c to 6876549 Compare June 16, 2025 06:26
@nicolas-delbovier-acsone
Copy link
Author

The last force push aimed at rebasing this PR to #1594.

Deletes an obsolete unit test that was migrated to the `sale_cart_quotation` module.
…ional custom workflow

This commit addresses a critical regression where the standard Odoo sales order confirmation flow was unintentionally broken, making it impossible to directly confirm a newly created SO.

The previous logic implicitly forced a custom quotation process even when not desired. This refactor introduces an explicit opt-in mechanism for the new quotation workflow.

Key changes:
- **Default Typology:** Removed a redundant test in `sale_typology` as the `typology` field's default value is now correctly handled and overridden by `sale_quotation`.
- **Workflow Toggle:** Added a `use_customer_quotation_workflow` boolean field to `sale.order`, allowing users to enable or disable the custom quotation stages.
- **Controlled Confirmation:** When `use_customer_quotation_workflow` is active, a UI wizard now prompts the user to confirm their intent if they attempt to confirm a quotation not yet in the 'waiting_acceptation' state. This preserves the standard Odoo workflow for non-workflow-enabled orders while providing a gentle warning for custom flows.
* add a button to accept the quotation
* add some margin to the workflow text
* Show quotation_state in the title instead of messing with the status bar
Changes `action_confirm_quotation` to `action_accept_quotation`.
@nicolas-delbovier-acsone nicolas-delbovier-acsone force-pushed the 16.0-refactor-quotations-rest-api branch from 71c7fab to b4e5b7b Compare July 9, 2025 12:48
lmignon added 7 commits July 14, 2025 08:59
Normal quotations are sale.order with an other typology than 'cart'. This allows the use of others values for typology than 'cart' and 'sale'.
Ensures menu are properly working if sale_cart or sale_quoration are installed but not the orher one
Use a switch button to activate / deactivate the user workflow for quotation. All the logic is now driven by the field and we no more need to manipulate the typology
Add methods triggered by the cutomer to manage the requested quotation. Ensures that methods called by customer are allowed according the the quotation state
Adds customer methods used to manage the quotation flow.
Ensures proper http status is returned in case where the called method is not allowed according to the quotation state
@lmignon lmignon marked this pull request as ready for review July 14, 2025 08:39
@lmignon lmignon force-pushed the 16.0-refactor-quotations-rest-api branch from c7a879b to 816d1c6 Compare July 16, 2025 10:20
lmignon added 9 commits July 16, 2025 16:26
The customer can only fill the client ref as rederence. The name is reserved for Odoo
Allows to search on quotation_state and client_order_ref
complete pot files are required to get the translations working. Indeed, when a transation file is loaded by the polib, the entry is marked as obsolete if it's not present into the pot file. obsolete entries are not loaded by odoo
@lmignon
Copy link
Collaborator

lmignon commented Sep 1, 2025

@paradoxxxzero fixed

Copy link
Contributor

@sebastienbeau sebastienbeau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the refactor ! Quotation flow will be more powerfull !

)


def InvalidQuotationStateErrorWrapper(func):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting code. Creating a specific error so we can catch it and return a clean http error code is smart.

In long term, maybe we should be able to extend the "convert_exception_to_status_body"
https://github.com/OCA/rest-framework/blob/34c06aab48512b6d7bda8ec797c45ae219a2fc59/fastapi/error_handlers.py#L20 so we can just extend this and it will raise the right http error without needed a wrapper.

The issue with the wrapper is when an other endpoint do an action and you didn't have set the decoration (because you didn't expect this case) you will not have the right http error.

Note a blocking point, it's just a comment to share the idea. If you think it's an interesting one we can to a PR in fastapi to implement it.

Comment on lines +229 to +236
def action_draft(self):
other_quotations = self
if not self.env.context.get("bypass_customer_quotation", False):
customer_quotations = self.filtered("use_customer_quotation_workflow")
other_quotations = self - customer_quotations
if customer_quotations:
customer_quotations.action_customer_reset_quotation_to_draft()
return super(SaleOrder, other_quotations).action_draft()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I always try to avoid function that call themself in both way (it's harder to read, and it can generate unexpected random issue)

For example if you have a custom module that do the following

def action_draft(self):
   for sale in self:
       sale._action_draft_count += 1
   super().action_draft()

Then depending on the order of loading the module, the increment code can be called before or after the inherit in sale_quotation.

In case that it's called before, if on a quotation you click on action_draft, it will increment a first time, then it will call "action_customer_reset_quotation_to_draft" that will call "action_draft" and increment a second time.

The crazy thing is that if the module are loaded in a different order (and the increment is call with the super), you will not have the issue, so have random bug (True bad experience).

Maybe we can simplify this logic and avoid the extra code of "bypass_customer_quotation"

Something like:

Suggested change
def action_draft(self):
other_quotations = self
if not self.env.context.get("bypass_customer_quotation", False):
customer_quotations = self.filtered("use_customer_quotation_workflow")
other_quotations = self - customer_quotations
if customer_quotations:
customer_quotations.action_customer_reset_quotation_to_draft()
return super(SaleOrder, other_quotations).action_draft()
def action_customer_reset_quotation_to_draft(self):
self._check_customer_action_allowed("reset_quotation_to_draft")
return self.action_draft()
def action_draft(self):
customer_quotations = self.filtered("use_customer_quotation_workflow")
customer_quotations.quotation_state = "draft"
customer_quotations.filtered(lambda so: so.typology != "quote").typology = "quote"
return super().action_draft()

What do you think ?

Copy link
Collaborator

@lmignon lmignon Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With your approach, the logic of resetting a customer quotation to draft is no more isolated into a dedicated method.... The current implementation also ensure that everything stay consistent if you click on the 'main' button for action_draft or on the customer one....

Copy link
Contributor

@sebastienbeau sebastienbeau Sep 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should have something like

    def _set_customer_reset_quotation_to_draft(self):
        self.quotation_state = "draft"
        self.filtered(lambda so: so.typology != "quote").typology = "quote"

    def action_customer_reset_quotation_to_draft(self):
        self._check_customer_action_allowed("reset_quotation_to_draft")
        self._set_customer_reset_quotation_to_draft()
        return self.with_context(skip_set_customer_reset_quotation_to_draft=True).action_draft()

    def action_draft(self):
        if not self._context.get("skip_set_customer_reset_quotation_to_draft"):
            self.filtered("use_customer_quotation_workflow")._set_customer_reset_quotation_to_draft()
        return super().action_draft()

Or

    def _set_customer_reset_quotation_to_draft(self):
        self.quotation_state = "draft"
        self.filtered(lambda so: so.typology != "quote").typology = "quote"

    def action_customer_reset_quotation_to_draft(self):
        self._check_customer_action_allowed("reset_quotation_to_draft")
        return self.action_draft()

    def action_draft(self):
        self.filtered("use_customer_quotation_workflow")._set_customer_reset_quotation_to_draft()
        return super().action_draft()

What do you think ?

def action_customer_reset_quotation_to_draft(self):
self._check_customer_action_allowed("reset_to_draft")
self.quotation_state = "draft"
self.state = "draft"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setting draft here is useless as action_draft will do it

Suggested change
self.state = "draft"

Comment on lines +181 to +183
return self.with_context(
bypass_customer_quotation=True,
).action_confirm()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change with the previous behavior.
Indeed before this change an accepted quotation was staying in draft (as maybe we wait for extra action like the payment)

The two customer we have, for now make this difference, they want to know that the customer have accepted the quotation, but they wait for the payment to confirm the sale order.

Maybe we can add a general configuration option ? So we can activate or not this behavior.

Comment on lines -68 to 73
<field
name="quotation_state"
widget="statusbar"
statusbar_visible="draft,waiting_acceptation,accepted"
attrs="{'invisible': [('state', 'in', ('sale', 'done'))]}"
name="use_customer_quotation_workflow"
widget="boolean_toggle"
options="{'autosave': False, 'terminology': 'Use Customer Quotation Workflow'}"
class="oe_inline"
/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following comment is just an opinion so nothing blocking

I do not like a lot to add extra status bar, on previous version I hide the original one and try to be more integrated with existing odoo action.

I fear that this make the UI harder to undertand (many button and status bar)

Screenshot 2025-09-04 at 12-34-58 Odoo - S00021

I will show it to @thibaultrey and @dora-jurcevic to get more feedback

…d upon creation

When a quotation is created via the API client, the assigned salesperson (user_id)
was not automatically added as a follower.

This occurred because the Odoo core mechanism for auto-subscribing the user_id
is only triggered when the 'user_id' value is explicitly present in the create
values dictionary. Since the API relies on Odoo's default value computation
to assign the salesperson, the auto-subscription logic was incorrectly bypassed.

This commit explicitly calls message_subscribe for the assigned user_id.partner_id
during the API creation flow, ensuring the salesperson receives relevant Chatter
notifications as expected.
if (
customer_quotations
and self.env.context.get("use_quotation_confirm_wizard")
and any(rec.quotation_state != "waiting_acceptation" for rec in self)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
and any(rec.quotation_state != "waiting_acceptation" for rec in self)
and any(rec.quotation_state != "waiting_acceptation" for rec in customer_quotations)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants