Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
from . import account_move
from . import automatic_workflow_job
from . import sale_order
from . import sale_workflow_process

View File

@@ -0,0 +1,14 @@
# Copyright 2011 Akretion Sébastien BEAU <sebastien.beau@akretion.com>
# Copyright 2013 Camptocamp SA (author: Guewen Baconnier)
# Copyright 2016 Sodexis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import fields, models
class AccountMove(models.Model):
_inherit = "account.move"
workflow_process_id = fields.Many2one(
comodel_name="sale.workflow.process", string="Sale Workflow Process", copy=False
)

View File

@@ -0,0 +1,223 @@
# Copyright 2011 Akretion Sébastien BEAU <sebastien.beau@akretion.com>
# Copyright 2013 Camptocamp SA (author: Guewen Baconnier)
# Copyright 2016 Sodexis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
import logging
from contextlib import contextmanager
from odoo import api, fields, models
from odoo.tools.safe_eval import safe_eval
_logger = logging.getLogger(__name__)
@contextmanager
def savepoint(cr):
"""Open a savepoint on the cursor, then yield.
Warning: using this method, the exceptions are logged then discarded.
"""
try:
with cr.savepoint():
yield
except Exception:
_logger.exception("Error during an automatic workflow action.")
class AutomaticWorkflowJob(models.Model):
"""Scheduler that will play automatically the validation of
invoices, pickings..."""
_name = "automatic.workflow.job"
_description = (
"Scheduler that will play automatically the validation of"
" invoices, pickings..."
)
def _do_validate_sale_order(self, sale, domain_filter):
"""Validate a sales order, filter ensure no duplication"""
if not self.env["sale.order"].search_count(
[("id", "=", sale.id)] + domain_filter
):
return f"{sale.display_name} {sale} job bypassed"
sale.action_confirm()
return f"{sale.display_name} {sale} confirmed successfully"
def _do_send_order_confirmation_mail(self, sale):
"""Send order confirmation mail, while filtering to make sure the order is
confirmed with _do_validate_sale_order() function"""
if not self.env["sale.order"].search_count(
[("id", "=", sale.id), ("state", "=", "sale")]
):
return f"{sale.display_name} {sale} job bypassed"
if sale.user_id:
sale = sale.with_user(sale.user_id)
sale._send_order_confirmation_mail()
return f"{sale.display_name} {sale} send order confirmation mail successfully"
@api.model
def _validate_sale_orders(self, order_filter):
sale_obj = self.env["sale.order"]
sales = sale_obj.search(order_filter)
_logger.debug("Sale Orders to validate: %s", sales.ids)
for sale in sales:
with savepoint(self.env.cr):
self._do_validate_sale_order(
sale.with_company(sale.company_id), order_filter
)
if self.env.context.get("send_order_confirmation_mail"):
self._do_send_order_confirmation_mail(sale)
def _do_create_invoice(self, sale, domain_filter):
"""Create an invoice for a sales order, filter ensure no duplication"""
if not self.env["sale.order"].search_count(
[("id", "=", sale.id)] + domain_filter
):
return f"{sale.display_name} {sale} job bypassed"
payment = self.env["sale.advance.payment.inv"].create(
{"sale_order_ids": sale.ids}
)
payment.with_context(active_model="sale.order").create_invoices()
return f"{sale.display_name} {sale} create invoice successfully"
@api.model
def _create_invoices(self, create_filter):
sale_obj = self.env["sale.order"]
sales = sale_obj.search(create_filter)
_logger.debug("Sale Orders to create Invoice: %s", sales.ids)
for sale in sales:
with savepoint(self.env.cr):
self._do_create_invoice(
sale.with_company(sale.company_id), create_filter
)
def _do_validate_invoice(self, invoice, domain_filter):
"""Validate an invoice, filter ensure no duplication"""
if not self.env["account.move"].search_count(
[("id", "=", invoice.id)] + domain_filter
):
return f"{invoice.display_name} {invoice} job bypassed"
invoice.with_company(invoice.company_id).action_post()
return f"{invoice.display_name} {invoice} validate invoice successfully"
@api.model
def _validate_invoices(self, validate_invoice_filter):
move_obj = self.env["account.move"]
invoices = move_obj.search(validate_invoice_filter)
_logger.debug("Invoices to validate: %s", invoices.ids)
for invoice in invoices:
with savepoint(self.env.cr):
self._do_validate_invoice(
invoice.with_company(invoice.company_id), validate_invoice_filter
)
def _do_sale_done(self, sale, domain_filter):
"""Lock a sales order, filter ensure no duplication"""
if not self.env["sale.order"].search_count(
[("id", "=", sale.id)] + domain_filter
):
return f"{sale.display_name} {sale} job bypassed"
sale.action_lock()
return f"{sale.display_name} {sale} locked successfully"
@api.model
def _sale_done(self, sale_done_filter):
sales = self.env["sale.order"].search(sale_done_filter)
_logger.debug("Sale Orders to done: %s", sales.ids)
for sale in sales:
with savepoint(self.env.cr):
self._do_sale_done(sale.with_company(sale.company_id), sale_done_filter)
def _prepare_dict_account_payment(self, invoice):
partner_type = (
invoice.move_type in ("out_invoice", "out_refund")
and "customer"
or "supplier"
)
return {
"reconciled_invoice_ids": [(6, 0, invoice.ids)],
"amount": invoice.amount_residual,
"partner_id": invoice.partner_id.id,
"partner_type": partner_type,
"date": fields.Date.context_today(self),
"currency_id": invoice.currency_id.id,
}
@api.model
def _register_payments(self, payment_filter):
invoice_obj = self.env["account.move"]
invoices = invoice_obj.search(payment_filter)
_logger.debug("Invoices to Register Payment: %s", invoices.ids)
for invoice in invoices:
with savepoint(self.env.cr):
self._register_payment_invoice(invoice)
return
def _register_payment_invoice(self, invoice):
payment = self.env["account.payment"].create(
self._prepare_dict_account_payment(invoice)
)
payment.action_post()
domain = [
("account_type", "in", ("asset_receivable", "liability_payable")),
("reconciled", "=", False),
]
payment_lines = payment.move_id.line_ids.filtered_domain(domain)
lines = invoice.line_ids
for account in payment_lines.account_id:
(payment_lines + lines).filtered_domain(
[("account_id", "=", account.id), ("reconciled", "=", False)]
).reconcile()
return payment
@api.model
def _handle_pickings(self, sale_workflow):
pass
def _sale_workflow_domain(self, workflow):
return [("workflow_process_id", "=", workflow.id)]
@api.model
def run_with_workflow(self, sale_workflow):
workflow_domain = self._sale_workflow_domain(sale_workflow)
if sale_workflow.validate_order:
self.with_context(
send_order_confirmation_mail=sale_workflow.send_order_confirmation_mail
)._validate_sale_orders(
safe_eval(sale_workflow.order_filter_id.domain) + workflow_domain
)
self._handle_pickings(sale_workflow)
if sale_workflow.create_invoice:
self._create_invoices(
safe_eval(sale_workflow.create_invoice_filter_id.domain)
+ workflow_domain
)
if sale_workflow.validate_invoice:
self._validate_invoices(
safe_eval(sale_workflow.validate_invoice_filter_id.domain)
+ workflow_domain
)
if sale_workflow.sale_done:
self._sale_done(
safe_eval(sale_workflow.sale_done_filter_id.domain) + workflow_domain
)
if sale_workflow.register_payment:
self._register_payments(
safe_eval(sale_workflow.payment_filter_id.domain) + workflow_domain
)
@api.model
def _workflow_process_to_run_domain(self):
return []
@api.model
def run(self):
"""Must be called from ir.cron"""
sale_workflow_process = self.env["sale.workflow.process"]
domain = self._workflow_process_to_run_domain()
for sale_workflow in sale_workflow_process.search(domain):
self.run_with_workflow(sale_workflow)
return True

View File

@@ -0,0 +1,89 @@
# Copyright 2011 Akretion Sébastien BEAU <sebastien.beau@akretion.com>
# Copyright 2013 Camptocamp SA (author: Guewen Baconnier)
# Copyright 2016 Sodexis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
from odoo.tools import float_compare
class SaleOrder(models.Model):
_inherit = "sale.order"
workflow_process_id = fields.Many2one(
comodel_name="sale.workflow.process",
string="Automatic Workflow",
ondelete="restrict",
copy=False,
)
all_qty_delivered = fields.Boolean(
compute="_compute_all_qty_delivered",
string="All quantities delivered",
store=True,
)
@api.depends("order_line.qty_delivered", "order_line.product_uom_qty")
def _compute_all_qty_delivered(self):
precision = self.env["decimal.precision"].precision_get(
"Product Unit of Measure"
)
for order in self:
order.all_qty_delivered = all(
line.product_id.type == "service"
or float_compare(
line.qty_delivered, line.product_uom_qty, precision_digits=precision
)
== 0
for line in order.order_line
)
def _prepare_invoice(self):
invoice_vals = super()._prepare_invoice()
workflow = self.workflow_process_id
if not workflow:
return invoice_vals
invoice_vals["workflow_process_id"] = workflow.id
if workflow.invoice_date_is_order_date:
invoice_vals["invoice_date"] = fields.Date.context_today(
self, self.date_order
)
if workflow.property_journal_id:
invoice_vals["journal_id"] = workflow.property_journal_id.id
return invoice_vals
@api.onchange("workflow_process_id")
def _onchange_workflow_process_id(self):
if self.workflow_process_id.warning:
warning = {
"title": self.env._("Workflow Warning"),
"message": self.workflow_process_id.warning,
}
return {"warning": warning}
@api.depends("partner_id", "user_id", "workflow_process_id")
def _compute_team_id(self): # pylint: disable=W8110
super()._compute_team_id()
if self.workflow_process_id.team_id:
self.team_id = self.workflow_process_id.team_id.id
def _create_invoices(self, grouped=False, final=False, date=None):
for order in self:
if not order.workflow_process_id.invoice_service_delivery:
continue
for line in order.order_line:
if line.qty_delivered_method == "manual" and not line.qty_delivered:
line.write({"qty_delivered": line.product_uom_qty})
return super()._create_invoices(grouped=grouped, final=final, date=date)
def write(self, vals):
if vals.get("state") == "sale" and vals.get("date_order"):
sales_keep_order_date = self.filtered(
lambda sale: sale.workflow_process_id.invoice_date_is_order_date
)
if sales_keep_order_date:
new_vals = vals.copy()
del new_vals["date_order"]
res = super(SaleOrder, sales_keep_order_date).write(new_vals)
res |= super(SaleOrder, self - sales_keep_order_date).write(vals)
return res
return super().write(vals)

View File

@@ -0,0 +1,113 @@
# Copyright 2011 Akretion Sébastien BEAU <sebastien.beau@akretion.com>
# Copyright 2013 Camptocamp SA (author: Guewen Baconnier)
# Copyright 2016 Sodexis
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from odoo import api, fields, models
class SaleWorkflowProcess(models.Model):
"""A workflow process is the setup of the automation of a sales order.
Each sales order can be linked to a workflow process.
Then, the options of the workflow will change how the sales order
behave, and how it is automatized.
A workflow process may be linked with a Sales payment method, so
each time a payment method is used, the workflow will be applied.
"""
_name = "sale.workflow.process"
_description = "Sale Workflow Process"
@api.model
def _default_filter(self, xmlid):
record = self.env.ref(xmlid, raise_if_not_found=False)
if record:
return record
return self.env["ir.filters"].browse()
name = fields.Char(required=True)
validate_order = fields.Boolean()
send_order_confirmation_mail = fields.Boolean(
help="When checked, after order confirmation, a confirmation email will be "
"sent (if not already sent).",
)
order_filter_domain = fields.Text(
string="Order Filter Domain", related="order_filter_id.domain"
)
create_invoice = fields.Boolean()
create_invoice_filter_domain = fields.Text(
string="Create Invoice Filter Domain", related="create_invoice_filter_id.domain"
)
validate_invoice = fields.Boolean()
validate_invoice_filter_domain = fields.Text(
string="Validate Invoice Filter Domain",
related="validate_invoice_filter_id.domain",
)
invoice_date_is_order_date = fields.Boolean(
string="Force Invoice Date",
help="When checked, the invoice date will be " "the same than the order's date",
)
invoice_service_delivery = fields.Boolean(
string="Invoice Service on delivery",
help="If this box is checked, when the first invoice is created "
"The service sale order lines will be included and will be "
"marked as delivered",
)
sale_done = fields.Boolean()
sale_done_filter_domain = fields.Text(
string="Sale Done Filter Domain", related="sale_done_filter_id.domain"
)
warning = fields.Text(
"Warning Message",
translate=True,
help="If set, displays the message when an user"
"selects the process on a sale order",
)
team_id = fields.Many2one(comodel_name="crm.team", string="Sales Team")
property_journal_id = fields.Many2one(
comodel_name="account.journal",
company_dependent=True,
string="Sales Journal",
help="Set default journal to use on invoice",
)
order_filter_id = fields.Many2one(
"ir.filters",
default=lambda self: self._default_filter(
"sale_automatic_workflow.automatic_workflow_order_filter"
),
)
create_invoice_filter_id = fields.Many2one(
"ir.filters",
string="Create Invoice Filter",
default=lambda self: self._default_filter(
"sale_automatic_workflow.automatic_workflow_create_invoice_filter"
),
)
validate_invoice_filter_id = fields.Many2one(
"ir.filters",
string="Validate Invoice Filter",
default=lambda self: self._default_filter(
"sale_automatic_workflow." "automatic_workflow_validate_invoice_filter"
),
)
sale_done_filter_id = fields.Many2one(
"ir.filters",
string="Sale Done Filter",
default=lambda self: self._default_filter(
"sale_automatic_workflow.automatic_workflow_sale_done_filter"
),
)
payment_filter_id = fields.Many2one(
comodel_name="ir.filters",
string="Register Payment Invoice Filter",
default=lambda self: self._default_filter(
"sale_automatic_workflow.automatic_workflow_payment_filter"
),
)
register_payment = fields.Boolean()
payment_filter_domain = fields.Text(
related="payment_filter_id.domain",
)