Initial commit: Odoo 18.0-20251222 extra-addons
This commit is contained in:
4
sale_automatic_workflow/models/__init__.py
Executable file
4
sale_automatic_workflow/models/__init__.py
Executable file
@@ -0,0 +1,4 @@
|
||||
from . import account_move
|
||||
from . import automatic_workflow_job
|
||||
from . import sale_order
|
||||
from . import sale_workflow_process
|
||||
14
sale_automatic_workflow/models/account_move.py
Executable file
14
sale_automatic_workflow/models/account_move.py
Executable 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
|
||||
)
|
||||
223
sale_automatic_workflow/models/automatic_workflow_job.py
Executable file
223
sale_automatic_workflow/models/automatic_workflow_job.py
Executable 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
|
||||
89
sale_automatic_workflow/models/sale_order.py
Executable file
89
sale_automatic_workflow/models/sale_order.py
Executable 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)
|
||||
113
sale_automatic_workflow/models/sale_workflow_process.py
Executable file
113
sale_automatic_workflow/models/sale_workflow_process.py
Executable 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",
|
||||
)
|
||||
Reference in New Issue
Block a user