mirror of
https://github.com/bringout/oca-ocb-sale.git
synced 2026-04-26 10:12:06 +02:00
19.0 vanilla
This commit is contained in:
parent
79f83631d5
commit
73afc09215
6267 changed files with 1534193 additions and 1130106 deletions
|
|
@ -1,9 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import account_payment_register
|
||||
from . import mail_compose_message
|
||||
from . import payment_provider_onboarding_wizard
|
||||
from . import base_document_layout
|
||||
from . import mass_cancel_orders
|
||||
from . import payment_link_wizard
|
||||
from . import res_config_settings
|
||||
from . import sale_make_invoice_advance
|
||||
from . import sale_order_cancel
|
||||
from . import sale_order_discount
|
||||
|
|
|
|||
|
|
@ -3,11 +3,19 @@
|
|||
|
||||
<record id="action_accrued_revenue_entry" model="ir.actions.act_window">
|
||||
<field name="name">Accrued Revenue Entry</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">account.accrued.orders.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="groups_id" eval="[(4, ref('account.group_account_user'))]"/>
|
||||
<field name="group_ids" eval="[(4, ref('account.group_account_user'))]"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
<record id="action_accrued_revenue_entry_sale_order_line" model="ir.actions.act_window">
|
||||
<field name="name">Accrued Revenue Entry</field>
|
||||
<field name="res_model">account.accrued.orders.wizard</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order_line"/>
|
||||
<field name="group_ids" eval="[(4, ref('account.group_account_user'))]"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class AccountPaymentRegister(models.TransientModel):
|
||||
_inherit = 'account.payment.register'
|
||||
|
||||
def _create_payment_vals_from_wizard(self, batch_result):
|
||||
vals = super()._create_payment_vals_from_wizard(batch_result)
|
||||
# Make sure the account move linked to generated payment
|
||||
# belongs to the expected sales team
|
||||
# team_id field on account.payment comes from the `_inherits` on account.move model
|
||||
vals.update({'team_id': self.line_ids.move_id[0].team_id.id})
|
||||
return vals
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class BaseDocumentLayout(models.TransientModel):
|
||||
_inherit = 'base.document.layout'
|
||||
|
||||
def _get_preview_template(self):
|
||||
if (
|
||||
self.env.context.get('active_model') == 'sale.order'
|
||||
and self.env.context.get('active_id')
|
||||
):
|
||||
return 'sale.quote_document_layout_preview'
|
||||
return super()._get_preview_template()
|
||||
|
||||
def _get_render_information(self, styles):
|
||||
res = super()._get_render_information(styles)
|
||||
if (
|
||||
self.env.context.get('active_model') == 'sale.order'
|
||||
and self.env.context.get('active_id')
|
||||
):
|
||||
res['doc'] = self.env['sale.order'].browse(self.env.context.get('active_id'))
|
||||
return res
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import models
|
||||
|
||||
|
||||
class MailComposeMessage(models.TransientModel):
|
||||
_inherit = 'mail.compose.message'
|
||||
|
||||
def _action_send_mail(self, auto_commit=False):
|
||||
if self.model == 'sale.order':
|
||||
self = self.with_context(mailing_document_based=True)
|
||||
if self.env.context.get('mark_so_as_sent'):
|
||||
self = self.with_context(mail_notify_author=self.env.user.partner_id in self.partner_ids)
|
||||
return super(MailComposeMessage, self)._action_send_mail(auto_commit=auto_commit)
|
||||
32
odoo-bringout-oca-ocb-sale/sale/wizard/mass_cancel_orders.py
Normal file
32
odoo-bringout-oca-ocb-sale/sale/wizard/mass_cancel_orders.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class SaleMassCancelOrders(models.TransientModel):
|
||||
_name = 'sale.mass.cancel.orders'
|
||||
_description = "Cancel multiple quotations"
|
||||
|
||||
sale_order_ids = fields.Many2many(
|
||||
string="Sale orders to cancel",
|
||||
comodel_name='sale.order',
|
||||
default=lambda self: self.env.context.get('active_ids'),
|
||||
relation='sale_order_mass_cancel_wizard_rel',
|
||||
)
|
||||
sale_orders_count = fields.Integer(compute='_compute_sale_orders_count')
|
||||
has_confirmed_order = fields.Boolean(compute='_compute_has_confirmed_order')
|
||||
|
||||
@api.depends('sale_order_ids')
|
||||
def _compute_sale_orders_count(self):
|
||||
for wizard in self:
|
||||
wizard.sale_orders_count = len(wizard.sale_order_ids)
|
||||
|
||||
@api.depends('sale_order_ids')
|
||||
def _compute_has_confirmed_order(self):
|
||||
for wizard in self:
|
||||
wizard.has_confirmed_order = bool(
|
||||
wizard.sale_order_ids.filtered(lambda so: so.state in ['sale', 'done'])
|
||||
)
|
||||
|
||||
def action_mass_cancel(self):
|
||||
self.sale_order_ids._action_cancel()
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="mass_cancel_orders_view_form" model="ir.ui.view">
|
||||
<field name="name">sale.mass.cancel.orders.form</field>
|
||||
<field name="model">sale.mass.cancel.orders</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Cancel quotations">
|
||||
<field name="sale_order_ids" invisible="1"/>
|
||||
<field name="sale_orders_count" invisible="1"/>
|
||||
<field name="has_confirmed_order" invisible="1"/>
|
||||
<div invisible="not has_confirmed_order">
|
||||
<p class="alert alert-warning fw-bold" role="alert">
|
||||
Some confirmed orders are selected. Their related documents might be
|
||||
affected by the cancellation.
|
||||
</p>
|
||||
</div>
|
||||
<div invisible="sale_orders_count > 1">
|
||||
Are you sure you want to cancel the selected item?
|
||||
</div>
|
||||
<div invisible="sale_orders_count == 1">
|
||||
So, are you sure you want to cancel these <field name="sale_orders_count"/> items?
|
||||
</div>
|
||||
<footer>
|
||||
<button class="btn-primary"
|
||||
name="action_mass_cancel"
|
||||
type="object"
|
||||
string="Cancel"/>
|
||||
<button string="Discard" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_mass_cancel_orders" model="ir.actions.act_window">
|
||||
<field name="name">Cancel</field>
|
||||
<field name="res_model">sale.mass.cancel.orders</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="mass_cancel_orders_view_form"/>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="model_sale_order"/>
|
||||
<field name="binding_view_types">list,kanban</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,42 +1,49 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from werkzeug import urls
|
||||
|
||||
from odoo import api, models
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class PaymentLinkWizard(models.TransientModel):
|
||||
_inherit = 'payment.link.wizard'
|
||||
_description = 'Generate Sales Payment Link'
|
||||
|
||||
def _get_payment_provider_available(self, res_model, res_id, **kwargs):
|
||||
""" Select and return the providers matching the criteria.
|
||||
amount_paid = fields.Monetary(string="Already Paid", readonly=True)
|
||||
prepayment_amount = fields.Monetary(string="Prepayment Amount", currency_field='currency_id')
|
||||
confirmation_message = fields.Char(
|
||||
string="Confirmation Message", compute='_compute_confirmation_message'
|
||||
)
|
||||
|
||||
:param str res_model: active model
|
||||
:param int res_id: id of 'active_model' record
|
||||
:return: The compatible providers
|
||||
:rtype: recordset of `payment.provider`
|
||||
"""
|
||||
if res_model == 'sale.order':
|
||||
kwargs['sale_order_id'] = res_id
|
||||
return super()._get_payment_provider_available(**kwargs)
|
||||
@api.depends('amount')
|
||||
def _compute_confirmation_message(self):
|
||||
self.confirmation_message = False
|
||||
for wizard in self.filtered(lambda w: w.res_model == 'sale.order'):
|
||||
sale_order = wizard.env['sale.order'].browse(wizard.res_id)
|
||||
if sale_order.state in ('draft', 'sent') and sale_order.require_payment:
|
||||
wizard.confirmation_message = _("This payment will confirm the quotation.")
|
||||
|
||||
def _get_additional_link_values(self):
|
||||
""" Override of `payment` to add `sale_order_id` to the payment link values.
|
||||
@api.depends('res_model', 'res_id')
|
||||
def _compute_warning_message(self):
|
||||
sale_wizards = self.env['payment.link.wizard']
|
||||
for wizard in self.filtered(lambda w: w.res_model == 'sale.order'):
|
||||
sale_order = wizard.env['sale.order'].browse(wizard.res_id)
|
||||
if sale_order.state in ('draft', 'sent') and wizard.amount < wizard.prepayment_amount:
|
||||
wizard.warning_message = _("The amount must be greater than the prepayment amount.")
|
||||
sale_wizards |= wizard # Prevent the super call from clearing the warning message.
|
||||
if sale_order.is_expired:
|
||||
wizard.warning_message = _("The sale order has expired.")
|
||||
sale_wizards |= wizard
|
||||
super(PaymentLinkWizard, self - sale_wizards)._compute_warning_message()
|
||||
|
||||
The other values related to the sales order are directly read from the sales order.
|
||||
def _prepare_url(self, base_url, related_document):
|
||||
""" Override of `payment` to use the portal page URL of sales orders. """
|
||||
if self.res_model == 'sale.order':
|
||||
return f'{base_url}{related_document.get_portal_url()}'
|
||||
else:
|
||||
return super()._prepare_url(base_url, related_document)
|
||||
|
||||
Note: self.ensure_one()
|
||||
|
||||
:return: The additional payment link values.
|
||||
:rtype: dict
|
||||
"""
|
||||
res = super()._get_additional_link_values()
|
||||
if self.res_model != 'sale.order':
|
||||
return res
|
||||
|
||||
# Order-related fields are retrieved in the controller
|
||||
return {
|
||||
'sale_order_id': self.res_id,
|
||||
}
|
||||
def _prepare_query_params(self, *args):
|
||||
""" Override of `payment` to add SO-related values to the query params. """
|
||||
if self.res_model == 'sale.order':
|
||||
return {'payment_amount': self.amount}
|
||||
else:
|
||||
return super()._prepare_query_params(*args)
|
||||
|
|
|
|||
|
|
@ -11,4 +11,25 @@
|
|||
<field name="binding_view_types">form</field>
|
||||
</record>
|
||||
|
||||
<record id="payment_link_wizard_view_form" model="ir.ui.view">
|
||||
<field name="name">payment.link.wizard.form</field>
|
||||
<field name="model">payment.link.wizard</field>
|
||||
<field name="inherit_id" ref="payment.payment_link_wizard_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<field name="amount" position="after">
|
||||
<field name="amount_paid" invisible="amount_paid <= 0"/>
|
||||
</field>
|
||||
<div name="payment_link_warning_information" position="after">
|
||||
<div
|
||||
name="payment_link_confirmation_message"
|
||||
class="alert alert-info"
|
||||
role="alert"
|
||||
invisible="warning_message or not confirmation_message"
|
||||
>
|
||||
<field name="confirmation_message"/>
|
||||
</div>
|
||||
</div>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,41 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class PaymentWizard(models.TransientModel):
|
||||
""" Override for the sale quotation onboarding panel. """
|
||||
|
||||
_inherit = 'payment.provider.onboarding.wizard'
|
||||
_name = 'sale.payment.provider.onboarding.wizard'
|
||||
_description = 'Sale Payment provider onboarding wizard'
|
||||
|
||||
def _get_default_payment_method(self):
|
||||
return self.env.company.sale_onboarding_payment_method or 'digital_signature'
|
||||
|
||||
payment_method = fields.Selection(selection_add=[
|
||||
('digital_signature', "Electronic signature"),
|
||||
('stripe', "Credit & Debit card (via Stripe)"),
|
||||
('paypal', "PayPal"),
|
||||
('manual', "Custom payment instructions"),
|
||||
], default=_get_default_payment_method)
|
||||
#
|
||||
|
||||
def _set_payment_provider_onboarding_step_done(self):
|
||||
""" Override. """
|
||||
self.env.company.sudo().set_onboarding_step_done('sale_onboarding_order_confirmation_state')
|
||||
|
||||
def add_payment_methods(self):
|
||||
self.env.company.sale_onboarding_payment_method = self.payment_method
|
||||
if self.payment_method == 'digital_signature':
|
||||
self.env.company.portal_confirmation_sign = True
|
||||
if self.payment_method in ('paypal', 'stripe', 'other', 'manual'):
|
||||
self.env.company.portal_confirmation_pay = True
|
||||
|
||||
return super().add_payment_methods()
|
||||
|
||||
def _start_stripe_onboarding(self):
|
||||
""" Override of payment to set the sale menu as start menu of the payment onboarding. """
|
||||
menu_id = self.env.ref('sale.sale_menu_root').id
|
||||
return self.env.company._run_payment_onboarding_step(menu_id)
|
||||
133
odoo-bringout-oca-ocb-sale/sale/wizard/res_config_settings.py
Normal file
133
odoo-bringout-oca-ocb-sale/sale/wizard/res_config_settings.py
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
|
||||
|
||||
class ResConfigSettings(models.TransientModel):
|
||||
_inherit = 'res.config.settings'
|
||||
|
||||
# Defaults
|
||||
default_invoice_policy = fields.Selection(
|
||||
selection=[
|
||||
('order', "Invoice what is ordered"),
|
||||
('delivery', "Invoice what is delivered")
|
||||
],
|
||||
string="Invoicing Policy",
|
||||
default='order',
|
||||
default_model='product.template')
|
||||
|
||||
# Groups
|
||||
group_auto_done_setting = fields.Boolean(
|
||||
string="Lock Confirmed Sales", implied_group='sale.group_auto_done_setting')
|
||||
group_discount_per_so_line = fields.Boolean(
|
||||
string="Discounts", implied_group='sale.group_discount_per_so_line')
|
||||
group_proforma_sales = fields.Boolean(
|
||||
string="Pro-Forma Invoice", implied_group='sale.group_proforma_sales',
|
||||
help="Allows you to send pro-forma invoice.")
|
||||
group_warning_sale = fields.Boolean(
|
||||
string="Sale Order Warnings", implied_group='sale.group_warning_sale')
|
||||
|
||||
# Config params
|
||||
automatic_invoice = fields.Boolean(
|
||||
string="Automatic Invoice",
|
||||
help="The invoice is generated automatically and available in the customer portal when the "
|
||||
"transaction is confirmed by the payment provider.\nThe invoice is marked as paid and "
|
||||
"the payment is registered in the payment journal defined in the configuration of the "
|
||||
"payment provider.\nThis mode is advised if you issue the final invoice at the order "
|
||||
"and not after the delivery.",
|
||||
config_parameter='sale.automatic_invoice',
|
||||
)
|
||||
|
||||
invoice_mail_template_id = fields.Many2one(
|
||||
comodel_name='mail.template',
|
||||
string="Email Template",
|
||||
domain=[('model', '=', 'account.move')],
|
||||
config_parameter='sale.default_invoice_email_template',
|
||||
help="Email sent to the customer once the invoice is available.",
|
||||
)
|
||||
quotation_validity_days = fields.Integer(
|
||||
related='company_id.quotation_validity_days',
|
||||
readonly=False)
|
||||
portal_confirmation_sign = fields.Boolean(
|
||||
related='company_id.portal_confirmation_sign',
|
||||
readonly=False)
|
||||
portal_confirmation_pay = fields.Boolean(
|
||||
related='company_id.portal_confirmation_pay',
|
||||
readonly=False)
|
||||
prepayment_percent = fields.Float(
|
||||
related='company_id.prepayment_percent',
|
||||
readonly=False)
|
||||
downpayment_account_id = fields.Many2one(related='company_id.downpayment_account_id', readonly=False)
|
||||
|
||||
# Modules
|
||||
module_delivery = fields.Boolean("Delivery Methods")
|
||||
module_delivery_bpost = fields.Boolean("bpost Connector")
|
||||
module_delivery_dhl = fields.Boolean("DHL Express Connector")
|
||||
module_delivery_easypost = fields.Boolean("Easypost Connector")
|
||||
module_delivery_envia = fields.Boolean("Envia.com Connector")
|
||||
module_delivery_fedex_rest = fields.Boolean("FedEx Connector")
|
||||
module_delivery_sendcloud = fields.Boolean("Sendcloud Connector")
|
||||
module_delivery_shiprocket = fields.Boolean("Shiprocket Connector")
|
||||
module_delivery_starshipit = fields.Boolean("Starshipit Connector")
|
||||
module_delivery_ups_rest = fields.Boolean("UPS Connector")
|
||||
module_delivery_usps_rest = fields.Boolean("USPS Connector")
|
||||
|
||||
module_product_email_template = fields.Boolean("Specific Email")
|
||||
module_sale_amazon = fields.Boolean("Amazon Sync")
|
||||
module_sale_commission = fields.Boolean("Commissions")
|
||||
module_sale_gelato = fields.Boolean("Gelato")
|
||||
module_sale_loyalty = fields.Boolean("Coupons & Loyalty")
|
||||
module_sale_margin = fields.Boolean("Margins")
|
||||
module_sale_pdf_quote_builder = fields.Boolean("PDF Quote builder")
|
||||
module_sale_product_matrix = fields.Boolean("Sales Grid Entry")
|
||||
module_sale_shopee = fields.Boolean("Shopee Sync")
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
@api.depends('group_discount_per_so_line')
|
||||
def _onchange_group_discount_per_so_line(self):
|
||||
if self.group_discount_per_so_line:
|
||||
self.group_product_pricelist = True
|
||||
|
||||
@api.onchange('group_product_variant')
|
||||
def _onchange_group_product_variant(self):
|
||||
"""The product Configurator requires the product variants activated.
|
||||
If the user disables the product variants -> disable the product configurator as well"""
|
||||
if self.module_sale_product_matrix and not self.group_product_variant:
|
||||
self.module_sale_product_matrix = False
|
||||
|
||||
@api.onchange('portal_confirmation_pay')
|
||||
def _onchange_portal_confirmation_pay(self):
|
||||
self.prepayment_percent = self.prepayment_percent or 1.0
|
||||
|
||||
@api.onchange('prepayment_percent')
|
||||
def _onchange_prepayment_percent(self):
|
||||
if not self.prepayment_percent:
|
||||
self.portal_confirmation_pay = False
|
||||
|
||||
@api.onchange('quotation_validity_days')
|
||||
def _onchange_quotation_validity_days(self):
|
||||
if self.quotation_validity_days < 0:
|
||||
self.quotation_validity_days = self.env['res.company'].default_get(
|
||||
['quotation_validity_days']
|
||||
)['quotation_validity_days']
|
||||
return {
|
||||
'warning': {
|
||||
'title': _("Warning"),
|
||||
'message': _("Quotation Validity is required and must be greater or equal to 0."),
|
||||
},
|
||||
}
|
||||
|
||||
#=== CRUD METHODS ===#
|
||||
|
||||
def set_values(self):
|
||||
super().set_values()
|
||||
if self.default_invoice_policy != 'order':
|
||||
self.env['ir.config_parameter'].set_param(key='sale.automatic_invoice', value=False)
|
||||
|
||||
# === ACTION METHODS === #
|
||||
|
||||
# Unique name to avoid colliding with `website_payment`.
|
||||
def action_sale_start_payment_onboarding(self):
|
||||
menu = self.env.ref('sale.menu_sale_general_settings', raise_if_not_found=False)
|
||||
return self._start_payment_onboarding(menu and menu.id)
|
||||
|
|
@ -0,0 +1,284 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.sale</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="priority" eval="10"/>
|
||||
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//form" position="inside">
|
||||
<app notApp="1" string="Sales" data-string="Sales" name="sale_management" groups="sales_team.group_sale_manager">
|
||||
<block title="Product Catalog" name="catalog_setting_container">
|
||||
<setting id="variant_options" help="Sell variants of a product using attributes (size, color, etc.)" documentation="/applications/sales/sales/products_prices/products/variants.html">
|
||||
<field name="group_product_variant"/>
|
||||
<div class="content-group" invisible="not group_product_variant">
|
||||
<div class="mt8">
|
||||
<button name="%(product.attribute_action)d" icon="oi-arrow-right" type="action" string="Attributes" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting string="Variant Grid Entry" help="Add several variants to an order from a grid" id="product_matrix">
|
||||
<field name="module_sale_product_matrix"/>
|
||||
</setting>
|
||||
<setting id="uom_settings" help="Sell and purchase products in different units of measure or packagings">
|
||||
<field name="group_uom"/>
|
||||
<div class="content-group" invisible="not group_uom">
|
||||
<div class="mt8">
|
||||
<button name="%(uom.product_uom_form_action)d" icon="oi-arrow-right" type="action" string="Units & Packagings" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="email_template" title="Sending an email is useful if you need to share specific information or content about a product (instructions, rules, links, media, etc.). Create and set the email template from the product detail form (in Accounting tab)." string="Deliver Content by Email" help="Send a product-specific email once the invoice is validated">
|
||||
<field name="module_product_email_template"/>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Pricing" id="pricing_setting_container">
|
||||
<setting id="discount_sale_order_lines" title="Apply manual discounts on sales order lines or display discounts computed from pricelists (option to activate in the pricelist configuration)." help="Grant discounts on sales order lines">
|
||||
<field name="group_discount_per_so_line"/>
|
||||
</setting>
|
||||
<setting id="coupon_settings" title="Boost your sales with multiple kinds of programs: Coupons, Promotions, Gift Card, Loyalty. Specific conditions can be set (products, customers, minimum purchase amount, period). Rewards can be discounts (% or amount) or free products." string="Promotions, Loyalty & Gift Card" help="Manage Promotions, Coupons, Loyalty cards, Gift cards & eWallet">
|
||||
<field name="module_loyalty"/>
|
||||
</setting>
|
||||
<setting id="pricelist_configuration" documentation="/applications/sales/sales/products_prices/prices/pricing.html" help="Set multiple prices per product, automated discounts, etc.">
|
||||
<field name="group_product_pricelist"/>
|
||||
<div class="content-group" invisible="not group_product_pricelist">
|
||||
<div class="mt16">
|
||||
<button name="%(product.product_pricelist_action2)d" icon="oi-arrow-right" type="action" string="Pricelists" groups="product.group_product_pricelist" class="btn-link"/>
|
||||
</div>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="auth_signup_documents" title=" To send invitations in B2B mode, open a contact or select several ones in list view and click on 'Portal Access Management' option in the dropdown menu *Action*." help="Let your customers log in to see their documents">
|
||||
<field name="auth_signup_uninvited" class="o_light_label" widget="radio" options="{'horizontal': true}" required="True"/>
|
||||
</setting>
|
||||
<setting id="show_margins" help="Show margins on orders" title="The margin is computed as the sum of product sales prices minus the cost set in their detail form.">
|
||||
<field name="module_sale_margin"/>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Quotations & Orders" name="quotation_order_setting_container">
|
||||
<setting id="online_signature" company_dependent="1"
|
||||
documentation="/applications/sales/sales/send_quotations/get_signature_to_validate.html"
|
||||
help="Request customers to sign quotations to validate orders. The default can be changed per order or template.">
|
||||
<field name="portal_confirmation_sign"/>
|
||||
</setting>
|
||||
<setting id="online_payment" company_dependent="1"
|
||||
documentation="/applications/sales/sales/send_quotations/get_paid_to_validate.html"
|
||||
help="Request a payment to confirm orders, in full (100%) or partial. The default can be changed per order or template.">
|
||||
<field name="portal_confirmation_pay"/>
|
||||
<div invisible="not portal_confirmation_pay" class="oe_row fw-bold">
|
||||
Payment
|
||||
<field name="prepayment_percent" widget="percentage" class="oe_inline"/>
|
||||
</div>
|
||||
<div
|
||||
class="row mt8"
|
||||
invisible="not portal_confirmation_pay or active_provider_id"
|
||||
>
|
||||
<div invisible="not onboarding_payment_module" class="oe_inline">
|
||||
<button
|
||||
type="object"
|
||||
name="action_sale_start_payment_onboarding"
|
||||
class="btn-primary"
|
||||
>
|
||||
Activate
|
||||
<field
|
||||
name="onboarding_payment_module"
|
||||
nolabel="1"
|
||||
class="oe_inline"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
title="Stripe Connect is not available in your country, please use another payment provider."
|
||||
invisible="onboarding_payment_module"
|
||||
class="oe_inline"
|
||||
>
|
||||
<button
|
||||
string="Activate Stripe" class="btn btn-primary" disabled=""
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
string="View Alternatives"
|
||||
type="action"
|
||||
name='%(payment.action_payment_provider)d'
|
||||
icon="oi-arrow-right"
|
||||
class="btn btn-link oe_inline"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="mt8"
|
||||
invisible="not portal_confirmation_pay or not active_provider_id"
|
||||
>
|
||||
<button
|
||||
type="object"
|
||||
name="action_view_active_provider"
|
||||
class="btn btn-primary col-auto"
|
||||
>
|
||||
Configure
|
||||
<field
|
||||
name="active_provider_id"
|
||||
class="oe_inline"
|
||||
nolabel="1"
|
||||
options="{'no_open': True}"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
string="View Other Providers"
|
||||
type="action"
|
||||
name="%(payment.action_payment_provider)d"
|
||||
icon="oi-arrow-right"
|
||||
class="btn btn-link col-auto"
|
||||
/>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="quotation_validity_days">
|
||||
<div title="Days between quotation proposal and expiration. 0 days means automatic expiration is disabled">
|
||||
<label for="quotation_validity_days"/>
|
||||
<field name="quotation_validity_days" class="text-center" style="width: 3rem;"/>
|
||||
<div class="d-inline-block">days</div>
|
||||
<span class="fa fa-lg fa-building-o p-2"
|
||||
title="Values set here are company-specific."
|
||||
groups="base.group_multi_company"/>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
Default period during which the quote is valid and can still be accepted by the customer. The default can be changed per order or template.
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="order_warnings" string="Sale Warnings" help="Get warnings in orders for products or customers">
|
||||
<field name="group_warning_sale"/>
|
||||
</setting>
|
||||
<setting id="sale_pdf_quote_builder" string="PDF Quote builder" help="Make your quote attractive by adding header pages, product descriptions and footer pages to your quote.">
|
||||
<field name="module_sale_pdf_quote_builder"/>
|
||||
<div class="mt8" name="sale_pdf_module_settings" invisible="not module_sale_pdf_quote_builder"/>
|
||||
</setting>
|
||||
<setting id="no_edit_order" help="No longer edit orders once confirmed">
|
||||
<field name="group_auto_done_setting"/>
|
||||
</setting>
|
||||
<setting id="proforma_configuration" help="Allows you to send Pro-Forma Invoice to your customers">
|
||||
<field name="group_proforma_sales"/>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Shipping" name="sale_shipping_setting_container">
|
||||
<setting id="delivery" help="Compute shipping costs on orders">
|
||||
<field name="module_delivery"/>
|
||||
</setting>
|
||||
<setting id="ups">
|
||||
<div class="o_form_label">UPS Connector</div>
|
||||
<a href="https://www.odoo.com/documentation/latest/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Compute shipping costs and ship with UPS<br/>
|
||||
<strong>(please go to Home>Apps to install)</strong>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="shipping_costs_dhl">
|
||||
<div class="o_form_label">DHL Connector</div>
|
||||
<a href="https://www.odoo.com/documentation/latest/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Compute shipping costs and ship with DHL<br/>
|
||||
<strong>(please go to Home>Apps to install)</strong>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="shipping_costs_fedex">
|
||||
<div class="o_form_label">FedEx Connector</div>
|
||||
<a href="https://www.odoo.com/documentation/latest/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Compute shipping costs and ship with FedEx<br/>
|
||||
<strong>(please go to Home>Apps to install)</strong>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="shipping_costs_usps">
|
||||
<div class="o_form_label">USPS Connector</div>
|
||||
<a href="https://www.odoo.com/documentation/latest/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html" title="Documentation" class="o_doc_link" target="_blank"></a>
|
||||
<div class="text-muted">
|
||||
Compute shipping costs and ship with USPS<br/>
|
||||
<strong>(please go to Home>Apps to install)</strong>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="shipping_costs_bpost" help="Compute shipping costs and ship with bpost"
|
||||
documentation="/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html">
|
||||
<field name="module_delivery_bpost" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting id="shipping_costs_easypost" help="Compute shipping costs and ship with Easypost"
|
||||
documentation="/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html">
|
||||
<field name="module_delivery_easypost" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting id="shipping_costs_sendcloud" help="Compute shipping costs and ship with Sendcloud"
|
||||
documentation="/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html">
|
||||
<field name="module_delivery_sendcloud" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting id="shipping_costs_shiprocket" help="Compute shipping costs and ship with Shiprocket"
|
||||
documentation="/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html">
|
||||
<field name="module_delivery_shiprocket" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting id="shipping_costs_starshipit" help="Compute shipping costs and ship with Starshipit"
|
||||
documentation="/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html">
|
||||
<field name="module_delivery_starshipit" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
<setting id="shipping_costs_envia" help="Compute shipping costs and ship with Envia.com"
|
||||
documentation="/applications/inventory_and_mrp/inventory/shipping/setup/third_party_shipper.html">
|
||||
<field name="module_delivery_envia" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Invoicing" name="invoicing_setting_container">
|
||||
<setting id="sales_settings_invoicing_policy" title="This default value is applied to any new product created. This can be changed in the product detail form." documentation="/applications/sales/sales/invoicing/invoicing_policy.html" help="Quantities to invoice from sales orders">
|
||||
<field name="default_invoice_policy" class="o_light_label" widget="radio"/>
|
||||
</setting>
|
||||
<setting id="automatic_invoicing" help="Generate the invoice automatically when the online payment is confirmed" invisible="default_invoice_policy != 'order' or not portal_confirmation_pay">
|
||||
<field name="automatic_invoice"/>
|
||||
<div invisible="not automatic_invoice" groups="base.group_no_one">
|
||||
<label for="invoice_mail_template_id" class="o_light_label me-2"/>
|
||||
<field name="invoice_mail_template_id" class="oe_inline" options="{'no_create': True}"/>
|
||||
</div>
|
||||
</setting>
|
||||
<setting id="setting_commission" help="Manage Sales & teams targets and commissions">
|
||||
<field name="module_sale_commission" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
</block>
|
||||
<block title="Connectors" id="connectors_setting_container">
|
||||
<setting id="amazon_connector" documentation="/applications/sales/sales/amazon_connector/setup.html" help="Import Amazon orders and sync deliveries">
|
||||
<field name="module_sale_amazon" widget="upgrade_boolean"/>
|
||||
<div class="content-group" name="amazon_connector" invisible="not module_sale_amazon"/>
|
||||
</setting>
|
||||
<setting
|
||||
id="gelato"
|
||||
documentation="/applications/sales/sales/gelato.html"
|
||||
help="Place orders through Gelato's print-on-demand service"
|
||||
>
|
||||
<field name="module_sale_gelato"/>
|
||||
<div
|
||||
class="content-group"
|
||||
name="gelato_credentials"
|
||||
invisible="not module_sale_gelato"
|
||||
/>
|
||||
</setting>
|
||||
<setting id="shopee_connector" help="Import Shopee orders and sync deliveries">
|
||||
<field name="module_sale_shopee" widget="upgrade_boolean"/>
|
||||
</setting>
|
||||
</block>
|
||||
</app>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="res_config_settings_view_form_sale_inherit" model="ir.ui.view">
|
||||
<field name="name">res.config.settings.view.form.inherit.sale</field>
|
||||
<field name="model">res.config.settings</field>
|
||||
<field name="inherit_id" ref="account.res_config_settings_view_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<xpath expr="//setting[@id='product_accounts']" position="inside">
|
||||
<div class="row mt8">
|
||||
<label for="downpayment_account_id" class="col-lg-5 o_light_label"/>
|
||||
<field name="downpayment_account_id"/>
|
||||
</div>
|
||||
</xpath>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_sale_config_settings" model="ir.actions.act_window">
|
||||
<field name="name">Settings</field>
|
||||
<field name="res_model">res.config.settings</field>
|
||||
<field name="view_id" ref="res_config_settings_view_form"/>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="context">{'module' : 'sale_management', 'bin_size': False}</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
@ -1,12 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
import time
|
||||
|
||||
from odoo import api, fields, models, _
|
||||
from odoo import SUPERUSER_ID, _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.fields import Command
|
||||
from odoo.tools import float_is_zero
|
||||
from odoo.tools import formatLang
|
||||
|
||||
|
||||
class SaleAdvancePaymentInv(models.TransientModel):
|
||||
|
|
@ -34,19 +31,12 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
|||
deduct_down_payments = fields.Boolean(string="Deduct down payments", default=True)
|
||||
|
||||
# New Down Payment
|
||||
product_id = fields.Many2one(
|
||||
comodel_name='product.product',
|
||||
string="Down Payment Product",
|
||||
domain=[('type', '=', 'service')],
|
||||
compute='_compute_product_id',
|
||||
readonly=False,
|
||||
store=True)
|
||||
amount = fields.Float(
|
||||
string="Down Payment Amount",
|
||||
help="The percentage of amount to be invoiced in advance, taxes excluded.")
|
||||
string="Down Payment",
|
||||
help="The percentage of amount to be invoiced in advance.")
|
||||
fixed_amount = fields.Monetary(
|
||||
string="Down Payment Amount (Fixed)",
|
||||
help="The fixed amount to be invoiced in advance, taxes excluded.")
|
||||
help="The fixed amount to be invoiced in advance.")
|
||||
currency_id = fields.Many2one(
|
||||
comodel_name='res.currency',
|
||||
compute='_compute_currency_id',
|
||||
|
|
@ -55,19 +45,18 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
|||
comodel_name='res.company',
|
||||
compute='_compute_company_id',
|
||||
store=True)
|
||||
amount_invoiced = fields.Monetary(
|
||||
string="Already invoiced",
|
||||
compute="_compute_invoice_amounts",
|
||||
help="Only confirmed down payments are considered.")
|
||||
|
||||
# Only used when there is no down payment product available
|
||||
# to setup the down payment product
|
||||
deposit_account_id = fields.Many2one(
|
||||
comodel_name='account.account',
|
||||
string="Income Account",
|
||||
domain=[('deprecated', '=', False)],
|
||||
help="Account used for deposits")
|
||||
deposit_taxes_id = fields.Many2many(
|
||||
comodel_name='account.tax',
|
||||
string="Customer Taxes",
|
||||
domain=[('type_tax_use', '=', 'sale')],
|
||||
help="Taxes used for deposits")
|
||||
# UI
|
||||
display_draft_invoice_warning = fields.Boolean(compute="_compute_display_draft_invoice_warning")
|
||||
consolidated_billing = fields.Boolean(
|
||||
string="Consolidated Billing", default=True,
|
||||
help="Create one invoice for all orders related to same customer, same invoicing address"
|
||||
" and same delivery address."
|
||||
)
|
||||
|
||||
#=== COMPUTE METHODS ===#
|
||||
|
||||
|
|
@ -99,16 +88,16 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
|||
if wizard.count == 1:
|
||||
wizard.company_id = wizard.sale_order_ids.company_id
|
||||
|
||||
@api.depends('company_id') # 'dumb' depends to trigger the computation
|
||||
def _compute_product_id(self):
|
||||
self.product_id = False
|
||||
dp_product_id = int(self.env['ir.config_parameter'].sudo().get_param(
|
||||
'sale.default_deposit_product_id'))
|
||||
if not dp_product_id:
|
||||
return
|
||||
@api.depends('sale_order_ids')
|
||||
def _compute_display_draft_invoice_warning(self):
|
||||
for wizard in self:
|
||||
if wizard.count == 1:
|
||||
wizard.product_id = dp_product_id
|
||||
invoice_states = wizard.sale_order_ids._origin.sudo().invoice_ids.mapped('state')
|
||||
wizard.display_draft_invoice_warning = 'draft' in invoice_states
|
||||
|
||||
@api.depends('sale_order_ids')
|
||||
def _compute_invoice_amounts(self):
|
||||
for wizard in self:
|
||||
wizard.amount_invoiced = sum(wizard.sale_order_ids._origin.mapped('amount_invoiced'))
|
||||
|
||||
#=== ONCHANGE METHODS ===#
|
||||
|
||||
|
|
@ -120,7 +109,6 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
|||
|
||||
#=== CONSTRAINT METHODS ===#
|
||||
|
||||
@api.constrains('advance_payment_method', 'amount', 'fixed_amount')
|
||||
def _check_amount_is_positive(self):
|
||||
for wizard in self:
|
||||
if wizard.advance_payment_method == 'percentage' and wizard.amount <= 0.00:
|
||||
|
|
@ -128,156 +116,131 @@ class SaleAdvancePaymentInv(models.TransientModel):
|
|||
elif wizard.advance_payment_method == 'fixed' and wizard.fixed_amount <= 0.00:
|
||||
raise UserError(_('The value of the down payment amount must be positive.'))
|
||||
|
||||
@api.constrains('product_id')
|
||||
def _check_down_payment_product_is_valid(self):
|
||||
for wizard in self:
|
||||
if wizard.count > 1 or not wizard.product_id:
|
||||
continue
|
||||
if wizard.product_id.invoice_policy != 'order':
|
||||
raise UserError(_(
|
||||
"The product used to invoice a down payment should have an invoice policy"
|
||||
"set to \"Ordered quantities\"."
|
||||
" Please update your deposit product to be able to create a deposit invoice."))
|
||||
if wizard.product_id.type != 'service':
|
||||
raise UserError(_(
|
||||
"The product used to invoice a down payment should be of type 'Service'."
|
||||
" Please use another product or update this product."))
|
||||
|
||||
#=== ACTION METHODS ===#
|
||||
|
||||
def create_invoices(self):
|
||||
self._create_invoices(self.sale_order_ids)
|
||||
self._check_amount_is_positive()
|
||||
invoices = self._create_invoices(self.sale_order_ids)
|
||||
return self.sale_order_ids.action_view_invoice(invoices=invoices)
|
||||
|
||||
if self.env.context.get('open_invoices'):
|
||||
return self.sale_order_ids.action_view_invoice()
|
||||
|
||||
return {'type': 'ir.actions.act_window_close'}
|
||||
def view_draft_invoices(self):
|
||||
return {
|
||||
'name': _('Draft Invoices'),
|
||||
'type': 'ir.actions.act_window',
|
||||
'view_mode': 'list',
|
||||
'views': [(False, 'list'), (False, 'form')],
|
||||
'res_model': 'account.move',
|
||||
'domain': [('line_ids.sale_line_ids.order_id', 'in', self.sale_order_ids.ids), ('state', '=', 'draft')],
|
||||
}
|
||||
|
||||
#=== BUSINESS METHODS ===#
|
||||
|
||||
def _create_invoices(self, sale_orders):
|
||||
self.ensure_one()
|
||||
if self.advance_payment_method == 'delivered':
|
||||
return sale_orders._create_invoices(final=self.deduct_down_payments)
|
||||
return sale_orders._create_invoices(final=self.deduct_down_payments, grouped=not self.consolidated_billing)
|
||||
else:
|
||||
self.sale_order_ids.ensure_one()
|
||||
self = self.with_company(self.company_id)
|
||||
order = self.sale_order_ids
|
||||
|
||||
# Create deposit product if necessary
|
||||
if not self.product_id:
|
||||
self.product_id = self.env['product.product'].create(
|
||||
self._prepare_down_payment_product_values()
|
||||
)
|
||||
self.env['ir.config_parameter'].sudo().set_param(
|
||||
'sale.default_deposit_product_id', self.product_id.id)
|
||||
AccountTax = self.env['account.tax']
|
||||
order_lines = order.order_line.filtered(lambda x: not x.display_type)
|
||||
base_lines = [line._prepare_base_line_for_taxes_computation() for line in order_lines]
|
||||
AccountTax._add_tax_details_in_base_lines(base_lines, order.company_id)
|
||||
AccountTax._round_base_lines_tax_details(base_lines, order.company_id)
|
||||
|
||||
# Create down payment section if necessary
|
||||
if not any(line.display_type and line.is_downpayment for line in order.order_line):
|
||||
self.env['sale.order.line'].create(
|
||||
self._prepare_down_payment_section_values(order)
|
||||
)
|
||||
if self.advance_payment_method == 'percentage':
|
||||
amount_type = 'percent'
|
||||
amount = self.amount
|
||||
else: # self.advance_payment_method == 'fixed':
|
||||
amount_type = 'fixed'
|
||||
amount = self.fixed_amount
|
||||
|
||||
down_payment_so_line = self.env['sale.order.line'].create(
|
||||
self._prepare_so_line_values(order)
|
||||
down_payment_base_lines = AccountTax._prepare_down_payment_lines(
|
||||
base_lines=base_lines,
|
||||
company=self.company_id,
|
||||
amount_type=amount_type,
|
||||
amount=amount,
|
||||
computation_key=f'down_payment,{self.id}',
|
||||
)
|
||||
|
||||
invoice = self.env['account.move'].sudo().create(
|
||||
self._prepare_invoice_values(order, down_payment_so_line)
|
||||
).with_user(self.env.uid) # Unsudo the invoice after creation
|
||||
# Update the sale order.
|
||||
order._create_down_payment_section_line_if_needed()
|
||||
so_lines = order._create_down_payment_lines_from_base_lines(down_payment_base_lines)
|
||||
|
||||
invoice.message_post_with_view(
|
||||
# Create the invoice.
|
||||
invoice_values = self.with_context(accounts=[
|
||||
base_line['account_id'] or self._get_down_payment_account(base_line['product_id'])
|
||||
for base_line in down_payment_base_lines
|
||||
])._prepare_down_payment_invoice_values(
|
||||
order=order,
|
||||
so_lines=so_lines,
|
||||
)
|
||||
invoice_sudo = self.env['account.move'].sudo().create(invoice_values)
|
||||
|
||||
# Unsudo the invoice after creation if not already sudoed
|
||||
invoice = invoice_sudo.sudo(self.env.su)
|
||||
poster = self.env.user._is_internal() and self.env.user.id or SUPERUSER_ID
|
||||
invoice.with_user(poster).message_post_with_source(
|
||||
'mail.message_origin_link',
|
||||
values={'self': invoice, 'origin': order},
|
||||
subtype_id=self.env.ref('mail.mt_note').id)
|
||||
render_values={'self': invoice, 'origin': order},
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
title = _("Down payment invoice")
|
||||
order.with_user(poster).message_post(
|
||||
body=_("%s has been created", invoice._get_html_link(title=title)),
|
||||
)
|
||||
|
||||
return invoice
|
||||
|
||||
def _prepare_down_payment_product_values(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
'name': _('Down payment'),
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'company_id': False,
|
||||
'property_account_income_id': self.deposit_account_id.id,
|
||||
'taxes_id': [Command.set(self.deposit_taxes_id.ids)],
|
||||
}
|
||||
|
||||
def _prepare_down_payment_section_values(self, order):
|
||||
context = {'lang': order.partner_id.lang}
|
||||
|
||||
so_values = {
|
||||
'name': _('Down Payments'),
|
||||
'product_uom_qty': 0.0,
|
||||
'order_id': order.id,
|
||||
'display_type': 'line_section',
|
||||
'is_downpayment': True,
|
||||
'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10,
|
||||
}
|
||||
|
||||
del context
|
||||
return so_values
|
||||
|
||||
def _prepare_so_line_values(self, order):
|
||||
self.ensure_one()
|
||||
analytic_distribution = {}
|
||||
amount_total = sum(order.order_line.mapped("price_total"))
|
||||
if not float_is_zero(amount_total, precision_rounding=self.currency_id.rounding):
|
||||
for line in order.order_line:
|
||||
distrib_dict = line.analytic_distribution or {}
|
||||
for account, distribution in distrib_dict.items():
|
||||
analytic_distribution[account] = distribution * line.price_total + analytic_distribution.get(account, 0)
|
||||
for account, distribution_amount in analytic_distribution.items():
|
||||
analytic_distribution[account] = distribution_amount/amount_total
|
||||
context = {'lang': order.partner_id.lang}
|
||||
so_values = {
|
||||
'name': _('Down Payment: %s (Draft)', time.strftime('%m %Y')),
|
||||
'price_unit': self._get_down_payment_amount(order),
|
||||
'product_uom_qty': 0.0,
|
||||
'order_id': order.id,
|
||||
'discount': 0.0,
|
||||
'product_id': self.product_id.id,
|
||||
'analytic_distribution': analytic_distribution,
|
||||
'is_downpayment': True,
|
||||
'sequence': order.order_line and order.order_line[-1].sequence + 1 or 10,
|
||||
}
|
||||
del context
|
||||
return so_values
|
||||
|
||||
def _get_down_payment_amount(self, order):
|
||||
self.ensure_one()
|
||||
if self.advance_payment_method == 'percentage':
|
||||
advance_product_taxes = self.product_id.taxes_id.filtered(lambda tax: tax.company_id == order.company_id)
|
||||
if all(order.fiscal_position_id.map_tax(advance_product_taxes).mapped('price_include')):
|
||||
amount = order.amount_total * self.amount / 100
|
||||
else:
|
||||
amount = order.amount_untaxed * self.amount / 100
|
||||
else: # Fixed amount
|
||||
amount = self.fixed_amount
|
||||
return amount
|
||||
|
||||
def _prepare_invoice_values(self, order, so_line):
|
||||
# TODO: add accounts to method params in master
|
||||
def _prepare_down_payment_invoice_values(self, order, so_lines):
|
||||
""" Prepare the values to create a down payment invoice.
|
||||
|
||||
:param order: The current sale order.
|
||||
:param so_lines: The "fake" down payment SO lines created on the sale order.
|
||||
:return: The values to create a new invoice.
|
||||
"""
|
||||
self.ensure_one()
|
||||
accounts = self.env.context.get('accounts')
|
||||
return {
|
||||
**order._prepare_invoice(),
|
||||
'invoice_line_ids': [
|
||||
Command.create(
|
||||
so_line._prepare_invoice_line(
|
||||
name=self._get_down_payment_description(order),
|
||||
quantity=1.0,
|
||||
)
|
||||
)
|
||||
Command.create(self._prepare_down_payment_invoice_line_values(order, so_line, self.company_id.downpayment_account_id or account))
|
||||
for so_line, account in zip(so_lines, accounts)
|
||||
],
|
||||
}
|
||||
|
||||
def _get_down_payment_description(self, order):
|
||||
self.ensure_one()
|
||||
context = {'lang': order.partner_id.lang}
|
||||
if self.advance_payment_method == 'percentage':
|
||||
name = _("Down payment of %s%%", self.amount)
|
||||
else:
|
||||
name = _('Down Payment')
|
||||
del context
|
||||
def _prepare_down_payment_invoice_line_values(self, order, so_line, account):
|
||||
""" Prepare the invoice line values to be part of a down payment invoice.
|
||||
|
||||
return name
|
||||
:param order: The current sale order.
|
||||
:param so_line: The "fake" down payment SO line created on the sale order.
|
||||
:param account: The down payment account to use.
|
||||
:return: The values to create a new invoice line.
|
||||
"""
|
||||
self.ensure_one()
|
||||
self = self.with_context(lang=order._get_lang())
|
||||
|
||||
if self.advance_payment_method == 'percentage':
|
||||
name = self.env._("Down payment of %s%%", formatLang(self.env, self.amount))
|
||||
else:
|
||||
name = self.env._("Down Payment")
|
||||
|
||||
return so_line._prepare_invoice_line(
|
||||
name=name,
|
||||
quantity=1.0,
|
||||
**({'account_id': account.id} if account else {}),
|
||||
)
|
||||
|
||||
def _get_down_payment_account(self, product):
|
||||
""" Retrieve the down payment account to use.
|
||||
:param product: A product.
|
||||
:return: An accounting account or None if not found.
|
||||
"""
|
||||
product_account = product.product_tmpl_id.get_product_accounts(
|
||||
fiscal_pos=self.sale_order_ids.fiscal_position_id
|
||||
)
|
||||
return product_account.get('downpayment') or product_account.get('income')
|
||||
|
|
|
|||
|
|
@ -6,77 +6,62 @@
|
|||
<field name="model">sale.advance.payment.inv</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Invoice Sales Order">
|
||||
<p class="oe_grey">
|
||||
Invoices will be created in draft so that you can review
|
||||
them before validation.
|
||||
</p>
|
||||
<field name="display_draft_invoice_warning" invisible="1"/>
|
||||
<field name="has_down_payments" invisible="1"/>
|
||||
<div class="alert alert-warning pb-1" role="alert" invisible="not display_draft_invoice_warning">
|
||||
<p>There are existing <a name="view_draft_invoices" type="object">Draft Invoices</a> for this Sale Order.</p>
|
||||
<p invisible="advance_payment_method != 'delivered'">
|
||||
The new invoice will deduct draft invoices linked to this sale order.
|
||||
</p>
|
||||
</div>
|
||||
<group>
|
||||
<field name="sale_order_ids" invisible="1"/>
|
||||
<field name="has_down_payments" invisible="1"/>
|
||||
<field name="count" attrs="{'invisible': [('count', '=', 1)]}"/>
|
||||
<field name="count" invisible="count == 1"/>
|
||||
<field name="consolidated_billing" invisible="count == 1"/>
|
||||
<field name="advance_payment_method" class="oe_inline"
|
||||
widget="radio"
|
||||
attrs="{'invisible': [('count', '>', 1)]}"/>
|
||||
<label for="deduct_down_payments"
|
||||
string=""
|
||||
attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"/>
|
||||
<div attrs="{'invisible': ['|', ('has_down_payments', '=', False), ('advance_payment_method', '!=', 'delivered')]}"
|
||||
id="down_payment_details">
|
||||
<field name="deduct_down_payments" nolabel="1"/>
|
||||
<label for="deduct_down_payments"/>
|
||||
</div>
|
||||
invisible="count > 1"/>
|
||||
</group>
|
||||
<group name="down_payment_specification"
|
||||
attrs="{'invisible': [('advance_payment_method', 'not in', ('fixed', 'percentage'))]}">
|
||||
invisible="advance_payment_method not in ('fixed', 'percentage')">
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="product_id" invisible="1"/>
|
||||
<label for="fixed_amount" attrs="{'invisible': [('advance_payment_method', '!=', 'fixed')]}"/>
|
||||
<label for="amount" attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}"/>
|
||||
<label for="fixed_amount" invisible="advance_payment_method != 'fixed'"/>
|
||||
<label for="amount" invisible="advance_payment_method != 'percentage'"/>
|
||||
<div id="payment_method_details">
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<field name="fixed_amount"
|
||||
attrs="{'required': [('advance_payment_method', '=', 'fixed')],
|
||||
'invisible': [('advance_payment_method', '!=', 'fixed')]}"
|
||||
invisible="advance_payment_method != 'fixed'"
|
||||
required="advance_payment_method == 'fixed'"
|
||||
class="oe_inline"/>
|
||||
<field name="amount"
|
||||
attrs="{'required': [('advance_payment_method', '=', 'percentage')],
|
||||
'invisible': [('advance_payment_method', '!=', 'percentage')]}"
|
||||
invisible="advance_payment_method != 'percentage'"
|
||||
required="advance_payment_method == 'percentage'"
|
||||
class="oe_inline"/>
|
||||
<span attrs="{'invisible': [('advance_payment_method', '!=', 'percentage')]}"
|
||||
class="oe_inline">%</span>
|
||||
<span invisible="advance_payment_method != 'percentage'"
|
||||
class="oe_inline">% </span>
|
||||
</div>
|
||||
<field name="deposit_account_id"
|
||||
options="{'no_create': True}"
|
||||
attrs="{'invisible': [('product_id', '!=', False)]}"
|
||||
groups="account.group_account_manager"/>
|
||||
<field name="deposit_taxes_id"
|
||||
widget="many2many_tags"
|
||||
attrs="{'invisible': [('product_id', '!=', False)]}"/>
|
||||
</group>
|
||||
<group invisible="not has_down_payments">
|
||||
<field name="amount_invoiced"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="create_invoices" type="object"
|
||||
id="create_invoice_open"
|
||||
string="Create and View Invoice"
|
||||
context="{'open_invoices': True}"
|
||||
string="Create Draft"
|
||||
class="btn-primary" data-hotkey="q"/>
|
||||
<button name="create_invoices" type="object"
|
||||
id="create_invoice"
|
||||
string="Create Invoice"
|
||||
data-hotkey="w"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="z"/>
|
||||
<button string="Cancel" class="btn-secondary" special="cancel" data-hotkey="x"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_view_sale_advance_payment_inv" model="ir.actions.act_window">
|
||||
<field name="name">Create invoices</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="name">Create invoice(s)</field>
|
||||
<field name="res_model">sale.advance.payment.inv</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="target">new</field>
|
||||
<field name="binding_model_id" ref="sale.model_sale_order"/>
|
||||
<field name="binding_view_types">list</field>
|
||||
<field name="binding_view_types">list,kanban</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class SaleOrderCancel(models.TransientModel):
|
||||
_name = 'sale.order.cancel'
|
||||
_inherit = 'mail.composer.mixin'
|
||||
_description = "Sales Order Cancel"
|
||||
|
||||
@api.model
|
||||
def _default_email_from(self):
|
||||
if self.env.user.email:
|
||||
return self.env.user.email_formatted
|
||||
raise UserError(_("Unable to post message, please configure the sender's email address."))
|
||||
|
||||
@api.model
|
||||
def _default_author_id(self):
|
||||
return self.env.user.partner_id
|
||||
|
||||
# origin
|
||||
email_from = fields.Char(string="From", default=_default_email_from)
|
||||
author_id = fields.Many2one(
|
||||
'res.partner',
|
||||
string="Author",
|
||||
index=True,
|
||||
ondelete='set null',
|
||||
default=_default_author_id,
|
||||
)
|
||||
|
||||
# recipients
|
||||
recipient_ids = fields.Many2many(
|
||||
'res.partner',
|
||||
string="Recipients",
|
||||
compute='_compute_recipient_ids',
|
||||
readonly=False,
|
||||
)
|
||||
order_id = fields.Many2one('sale.order', string="Sale Order", required=True, ondelete='cascade')
|
||||
display_invoice_alert = fields.Boolean(
|
||||
string="Invoice Alert",
|
||||
compute='_compute_display_invoice_alert',
|
||||
compute_sudo=True,
|
||||
)
|
||||
|
||||
@api.depends('order_id')
|
||||
def _compute_recipient_ids(self):
|
||||
for wizard in self:
|
||||
wizard.recipient_ids = wizard.order_id.partner_id \
|
||||
| wizard.order_id.message_partner_ids \
|
||||
- wizard.author_id
|
||||
|
||||
@api.depends('order_id')
|
||||
def _compute_display_invoice_alert(self):
|
||||
for wizard in self:
|
||||
wizard.display_invoice_alert = bool(
|
||||
wizard.order_id.invoice_ids.filtered(lambda inv: inv.state == 'draft')
|
||||
)
|
||||
|
||||
@api.depends('order_id')
|
||||
def _compute_subject(self):
|
||||
for wizard in self:
|
||||
if wizard.template_id:
|
||||
wizard.subject = wizard.template_id._render_field(
|
||||
'subject',
|
||||
wizard.order_id.ids,
|
||||
post_process=True,
|
||||
compute_lang=True,
|
||||
)[wizard.order_id.id]
|
||||
|
||||
@api.depends('order_id')
|
||||
def _compute_body(self):
|
||||
for wizard in self:
|
||||
if wizard.template_id:
|
||||
wizard.body = wizard.template_id._render_field(
|
||||
'body_html',
|
||||
wizard.order_id.ids,
|
||||
post_process=True,
|
||||
compute_lang=True,
|
||||
)[wizard.order_id.id]
|
||||
|
||||
def action_send_mail_and_cancel(self):
|
||||
self.ensure_one()
|
||||
self.order_id.message_post(
|
||||
subject=self.subject,
|
||||
body=self.body,
|
||||
message_type='comment',
|
||||
email_from=self.email_from,
|
||||
email_layout_xmlid='mail.mail_notification_light',
|
||||
partner_ids=self.recipient_ids.ids,
|
||||
)
|
||||
return self.action_cancel()
|
||||
|
||||
def action_cancel(self):
|
||||
return self.order_id.with_context(disable_cancel_warning=True).action_cancel()
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<record id="sale_order_cancel_view_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.cancel.form</field>
|
||||
<field name="model">sale.order.cancel</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group col="1">
|
||||
<field name="render_model" invisible="1"/>
|
||||
<field name="order_id" invisible="1"/>
|
||||
<field name="template_id" invisible="1"/>
|
||||
<field name="display_invoice_alert" invisible="1"/>
|
||||
<div col="2"
|
||||
class="alert alert-warning"
|
||||
role="alert">
|
||||
<span>Are you sure you want to cancel this order? <br/></span>
|
||||
<span id="display_invoice_alert"
|
||||
attrs="{'invisible': [('display_invoice_alert', '=', False)]}">
|
||||
Draft invoices for this order will be cancelled. <br/>
|
||||
</span>
|
||||
</div>
|
||||
<group col="2">
|
||||
<field name="recipient_ids"
|
||||
widget="many2many_tags_email"
|
||||
context="{'force_email': True,
|
||||
'show_email': True,
|
||||
'no_create_edit': True}"/>
|
||||
</group>
|
||||
<group col="2">
|
||||
<field name="subject" placeholder="Subject"/>
|
||||
</group>
|
||||
<field name="body"
|
||||
class="oe-bordered-editor"
|
||||
options="{'style-inline': true}"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button string="Send and cancel"
|
||||
name="action_send_mail_and_cancel"
|
||||
type="object"
|
||||
class="btn-primary"/>
|
||||
<button string="Cancel"
|
||||
name="action_cancel"
|
||||
type="object"
|
||||
class="btn-primary mx-1"/>
|
||||
<button string="Discard"
|
||||
class="btn-secondary"
|
||||
special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
166
odoo-bringout-oca-ocb-sale/sale/wizard/sale_order_discount.py
Normal file
166
odoo-bringout-oca-ocb-sale/sale/wizard/sale_order_discount.py
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from collections import defaultdict
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.fields import Command
|
||||
from odoo.tools import float_repr
|
||||
|
||||
|
||||
class SaleOrderDiscount(models.TransientModel):
|
||||
_name = 'sale.order.discount'
|
||||
_description = "Discount Wizard"
|
||||
|
||||
sale_order_id = fields.Many2one(
|
||||
'sale.order', default=lambda self: self.env.context.get('active_id'), required=True)
|
||||
company_id = fields.Many2one(related='sale_order_id.company_id')
|
||||
currency_id = fields.Many2one(related='sale_order_id.currency_id')
|
||||
discount_amount = fields.Monetary(string="Amount")
|
||||
discount_percentage = fields.Float(string="Percentage")
|
||||
discount_type = fields.Selection(
|
||||
selection=[
|
||||
('sol_discount', "On All Order Lines"),
|
||||
('so_discount', "Global Discount"),
|
||||
('amount', "Fixed Amount"),
|
||||
],
|
||||
default='sol_discount',
|
||||
)
|
||||
|
||||
# CONSTRAINT METHODS #
|
||||
|
||||
@api.constrains('discount_type', 'discount_percentage')
|
||||
def _check_discount_amount(self):
|
||||
for wizard in self:
|
||||
if (
|
||||
wizard.discount_type in ('sol_discount', 'so_discount')
|
||||
and wizard.discount_percentage > 1.0
|
||||
):
|
||||
raise ValidationError(_("Invalid discount amount"))
|
||||
|
||||
def _prepare_discount_product_values(self):
|
||||
self.ensure_one()
|
||||
values = {
|
||||
'name': _('Discount'),
|
||||
'type': 'service',
|
||||
'invoice_policy': 'order',
|
||||
'list_price': 0.0,
|
||||
'company_id': self.company_id.id,
|
||||
'taxes_id': None,
|
||||
}
|
||||
services_category = self.env.ref('product.product_category_services', raise_if_not_found=False)
|
||||
if services_category:
|
||||
values['categ_id'] = services_category.id
|
||||
return values
|
||||
|
||||
def _prepare_global_discount_so_lines(self, base_lines):
|
||||
self.ensure_one()
|
||||
AccountTax = self.env['account.tax']
|
||||
discount_dp = self.env['decimal.precision'].precision_get('Discount')
|
||||
has_multiple_tax_combinations = len(set(base_line['tax_ids'] for base_line in base_lines if base_line['tax_ids'])) > 1
|
||||
so_line_values_list = []
|
||||
for base_line in base_lines:
|
||||
|
||||
# The name of the so line.
|
||||
if has_multiple_tax_combinations:
|
||||
if self.discount_type == 'so_discount':
|
||||
so_line_description = self.env._(
|
||||
"Discount %(percent)s%%"
|
||||
"- On products with the following taxes %(taxes)s",
|
||||
percent=float_repr(self.discount_percentage * 100.0, discount_dp),
|
||||
taxes=", ".join(base_line['tax_ids'].mapped('name')),
|
||||
)
|
||||
else:
|
||||
so_line_description = self.env._(
|
||||
"Discount"
|
||||
"- On products with the following taxes %(taxes)s",
|
||||
taxes=", ".join(base_line['tax_ids'].mapped('name')),
|
||||
)
|
||||
else:
|
||||
if self.discount_type == 'so_discount':
|
||||
so_line_description = self.env._(
|
||||
"Discount %(percent)s%%",
|
||||
percent=float_repr(self.discount_percentage * 100.0, discount_dp),
|
||||
)
|
||||
else:
|
||||
so_line_description = self.env._("Discount")
|
||||
|
||||
so_line_values_list.append({
|
||||
'name': so_line_description,
|
||||
'product_id': base_line['product_id'].id,
|
||||
'price_unit': base_line['price_unit'],
|
||||
'technical_price_unit': 0,
|
||||
'product_uom_qty': base_line['quantity'],
|
||||
'tax_ids': [Command.set(base_line['tax_ids'].ids)],
|
||||
'extra_tax_data': AccountTax._export_base_line_extra_tax_data(base_line),
|
||||
'sequence': 999,
|
||||
})
|
||||
|
||||
return so_line_values_list
|
||||
|
||||
def _get_discount_product(self):
|
||||
"""Return product.product used for discount line"""
|
||||
self.ensure_one()
|
||||
company = self.company_id
|
||||
discount_product = company.sale_discount_product_id
|
||||
if not discount_product:
|
||||
if (
|
||||
self.env['product.product'].has_access('create')
|
||||
and company.has_access('write')
|
||||
and company._has_field_access(company._fields['sale_discount_product_id'], 'write')
|
||||
):
|
||||
company.sale_discount_product_id = self.env['product.product'].create(
|
||||
self._prepare_discount_product_values()
|
||||
)
|
||||
else:
|
||||
raise ValidationError(_(
|
||||
"There does not seem to be any discount product configured for this company yet."
|
||||
" You can either use a per-line discount, or ask an administrator to grant the"
|
||||
" discount the first time."
|
||||
))
|
||||
discount_product = company.sale_discount_product_id
|
||||
return discount_product
|
||||
|
||||
def _create_discount_lines(self):
|
||||
self.ensure_one()
|
||||
self = self.with_context(lang=self.sale_order_id._get_lang())
|
||||
|
||||
discount_product = self._get_discount_product()
|
||||
|
||||
if self.discount_type == 'so_discount':
|
||||
amount_type = 'percent'
|
||||
amount = self.discount_percentage * 100.0
|
||||
else: # self.discount_type == 'amount':
|
||||
amount_type = 'fixed'
|
||||
amount = self.discount_amount
|
||||
|
||||
order = self.sale_order_id
|
||||
AccountTax = self.env['account.tax']
|
||||
order_lines = order.order_line.filtered(lambda x: not x.display_type)
|
||||
base_lines = [line._prepare_base_line_for_taxes_computation() for line in order_lines]
|
||||
AccountTax._add_tax_details_in_base_lines(base_lines, order.company_id)
|
||||
AccountTax._round_base_lines_tax_details(base_lines, order.company_id)
|
||||
|
||||
def grouping_function(base_line):
|
||||
return {'product_id': discount_product}
|
||||
|
||||
global_discount_base_lines = AccountTax._prepare_global_discount_lines(
|
||||
base_lines=base_lines,
|
||||
company=self.company_id,
|
||||
amount_type=amount_type,
|
||||
amount=amount,
|
||||
computation_key=f'global_discount,{self.id}',
|
||||
grouping_function=grouping_function,
|
||||
)
|
||||
order.order_line = [
|
||||
Command.create(values)
|
||||
for values in self._prepare_global_discount_so_lines(global_discount_base_lines)
|
||||
]
|
||||
|
||||
def action_apply_discount(self):
|
||||
self.ensure_one()
|
||||
self = self.with_company(self.company_id)
|
||||
if self.discount_type == 'sol_discount':
|
||||
self.sale_order_id.order_line.write({'discount': self.discount_percentage * 100})
|
||||
else:
|
||||
self._create_discount_lines()
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<odoo>
|
||||
|
||||
<record id="sale_order_line_wizard_form" model="ir.ui.view">
|
||||
<field name="name">sale.order.discount.form</field>
|
||||
<field name="model">sale.order.discount</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<sheet>
|
||||
<field name="sale_order_id" invisible="1"/>
|
||||
<field name="company_id" invisible="1"/>
|
||||
<field name="currency_id" invisible="1"/>
|
||||
<div class="row">
|
||||
<div class="col-sm-5 col-md-4 col-lg-4 col-4">
|
||||
<group>
|
||||
<label for="discount_amount" string="Discount" invisible="discount_type != 'amount'"/>
|
||||
<field name="discount_amount" invisible="discount_type != 'amount'" nolabel="1"/>
|
||||
<label for="discount_percentage"
|
||||
string="Discount"
|
||||
invisible="discount_type not in ('so_discount', 'sol_discount')"/>
|
||||
<field name="discount_percentage"
|
||||
invisible="discount_type not in ('so_discount', 'sol_discount')"
|
||||
widget="percentage" nolabel="1"/>
|
||||
</group>
|
||||
</div>
|
||||
<div class="col-sm-7 col-md-8 col-lg-8 col-8">
|
||||
<field name="discount_type" widget="radio"/>
|
||||
</div>
|
||||
</div>
|
||||
</sheet>
|
||||
<footer>
|
||||
<button type="object" string="Apply" name="action_apply_discount" class="btn btn-primary" data-hotkey="q"/>
|
||||
<button special="cancel" string="Discard" class="btn btn-secondary" data-hotkey="x"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</odoo>
|
||||
Loading…
Add table
Add a link
Reference in a new issue