[REM] sale_coupon: remove sale_coupon module

Removes the sale_coupon module as it is merged inside of sale_loyalty.

TaskId-2675382
This commit is contained in:
William Braeckman
2022-04-08 09:38:43 +02:00
parent 56eaec5934
commit 4b07084d39
95 changed files with 0 additions and 94843 deletions
-5
View File
@@ -1,5 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import models
from . import wizard
-21
View File
@@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
{
'name': "Sale Coupon",
'summary': "Use discount coupons in sales orders",
'description': """Integrate coupon mechanism in sales orders.""",
'category': 'Sales/Sales',
'version': '1.0',
'depends': ['coupon', 'sale'],
'data': [
'data/mail_template_data.xml',
'security/sale_coupon_security.xml',
'security/ir.model.access.csv',
'wizard/sale_coupon_apply_code_views.xml',
'views/sale_order_views.xml',
'views/coupon_views.xml',
'views/coupon_program_views.xml',
'views/res_config_settings_views.xml',
],
'license': 'LGPL-3',
}
@@ -1,101 +0,0 @@
<?xml version="1.0" ?>
<odoo>
<data noupdate="1">
<record id="sale_coupon.mail_template_sale_coupon" model="mail.template">
<field name="name">[Sales] Coupon: Send by Email</field>
<field name="model_id" ref="coupon.model_coupon_coupon"/>
<field name="subject">Your reward coupon from {{ object.program_id.company_id.name }} </field>
<field name="email_from">{{ object.program_id.company_id.email }}</field>
<field name="partner_to">{{ object.order_id.partner_id.id or object.partner_id.id }}</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="body_html" type="html">
<table border="0" cellpadding="0" cellspacing="0" style="width:100%; margin:0px auto;"><tbody>
<tr><td valign="top" style="text-align: center; font-size: 14px;">
<t t-if="object.partner_id.name">
Congratulations <t t-out="object.partner_id.name or ''">Brandon Freeman</t>,<br />
</t>
Here is your reward from <t t-out="object.program_id.company_id.name or ''">YourCompany</t>.<br />
<t t-if="object.program_id.reward_type == 'discount'">
<t t-if="object.program_id.discount_type == 'fixed_amount'">
<span style="font-size: 50px; color: #875A7B; font-weight: bold;" t-out="'%s' % format_amount(object.program_id.discount_fixed_amount, object.program_id.currency_id) or ''">$ 10.00</span><br />
<strong style="font-size: 24px;">off on your next order</strong><br />
</t>
<t t-else="">
<span style="font-size: 50px; color: #875A7B; font-weight: bold;"><t t-out="object.program_id.discount_percentage or ''">10</t> %</span>
</t>
<t t-if="object.program_id.discount_apply_on == 'specific_products'">
<br />
<t t-if="len(object.program_id.discount_specific_product_ids) != 1">
<t t-set="display_specific_products" t-value="True"/>
<strong style="font-size: 24px;">
on some products*
</strong>
</t>
<t t-else="">
<strong style="font-size: 24px;" t-out="'on %s' % object.program_id.discount_specific_product_ids.name or ''">Customizable Desk</strong>
</t>
</t>
<t t-elif="object.program_id.discount_apply_on == 'cheapest_product'">
<br /><strong style="font-size: 24px;">
off on the cheapest product
</strong>
</t>
<t t-else="">
<br /><strong style="font-size: 24px;">
off on your next order
</strong>
</t>
<br />
</t>
<t t-elif="object.program_id.reward_type == 'product'">
<span style="font-size: 36px; color: #875A7B; font-weight: bold;" t-out="'get %s free %s' % (object.program_id.reward_product_quantity, object.program_id.reward_product_id.name) or ''">get 1 free Corner Desk Right Sit</span><br />
<strong style="font-size: 24px;">on your next order</strong><br />
</t>
<t t-elif="object.program_id.reward_type == 'free_shipping'">
<span style="font-size: 36px; color: #875A7B; font-weight: bold;">
get free shipping
</span><br />
<strong style="font-size: 24px;">on your next order</strong><br />
</t>
</td></tr>
<tr style="margin-top: 16px"><td valign="top" style="text-align: center; font-size: 14px;">
Use this promo code
<t t-if="object.expiration_date">
before <t t-out="object.expiration_date or ''">2021-06-16</t>
</t>
<p style="margin-top: 16px;">
<strong style="padding: 16px 8px 16px 8px; border-radius: 3px; background-color: #F1F1F1;" t-out="object.code or ''">15637502648479132902</strong>
</p>
<t t-if="object.program_id.rule_min_quantity not in [0, 1]">
<span style="font-size: 14px;">
Minimum purchase of <t t-out="object.program_id.rule_min_quantity or ''">10</t> products
</span><br />
</t>
<t t-if="object.program_id.rule_minimum_amount != 0.00">
<span style="font-size: 14px;">
Valid for purchase above <t t-out="object.program_id.company_id.currency_id.symbol or ''">$</t><t t-out="'%0.2f' % float(object.program_id.rule_minimum_amount) or ''">10.00</t>
</span><br />
</t>
<t t-if="display_specific_products">
<span>
*Valid for following products: <t t-out="', '.join(object.program_id.discount_specific_product_ids.mapped('name')) or ''">Customizable Desk</t>
</span><br />
</t>
<br/>
Thank you,
<t t-if="object.order_id.user_id.signature">
<br />
<t t-out="object.order_id.user_id.signature or ''">--<br/>Mitchell Admin</t>
</t>
</td></tr>
</tbody></table>
</field>
<field name="report_template" ref="coupon.report_coupon_code"/>
<field name="report_name">Your Coupon Code</field>
<field name="lang">{{ object.partner_id.lang }}</field>
<field name="auto_delete" eval="True"/>
</record>
</data>
</odoo>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-453
View File
@@ -1,453 +0,0 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_coupon
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2021-10-05 10:54+0000\n"
"PO-Revision-Date: 2021-10-05 10:54+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_coupon_view_coupon_program_kanban
msgid "<strong>Sales</strong>"
msgstr ""
#. module: sale_coupon
#: model:mail.template,body_html:sale_coupon.mail_template_sale_coupon
msgid ""
"<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"width:100%; margin:0px auto;\"><tbody>\n"
" <tr><td valign=\"top\" style=\"text-align: center; font-size: 14px;\">\n"
" <t t-if=\"object.partner_id.name\">\n"
" Congratulations <t t-out=\"object.partner_id.name or ''\">Brandon Freeman</t>,<br/>\n"
" </t>\n"
"\n"
" Here is your reward from <t t-out=\"object.program_id.company_id.name or ''\">YourCompany</t>.<br/>\n"
"\n"
" <t t-if=\"object.program_id.reward_type == 'discount'\">\n"
" <t t-if=\"object.program_id.discount_type == 'fixed_amount'\">\n"
" <span style=\"font-size: 50px; color: #875A7B; font-weight: bold;\" t-out=\"'%s' % format_amount(object.program_id.discount_fixed_amount, object.program_id.currency_id) or ''\">$ 10.00</span><br/>\n"
" <strong style=\"font-size: 24px;\">off on your next order</strong><br/>\n"
" </t>\n"
" <t t-else=\"\">\n"
" <span style=\"font-size: 50px; color: #875A7B; font-weight: bold;\"><t t-out=\"object.program_id.discount_percentage or ''\">10</t> %</span>\n"
" </t>\n"
" <t t-if=\"object.program_id.discount_apply_on == 'specific_products'\">\n"
" <br/>\n"
" <t t-if=\"len(object.program_id.discount_specific_product_ids) != 1\">\n"
" <t t-set=\"display_specific_products\" t-value=\"True\"/>\n"
" <strong style=\"font-size: 24px;\">\n"
" on some products*\n"
" </strong>\n"
" </t>\n"
" <t t-else=\"\">\n"
" <strong style=\"font-size: 24px;\" t-out=\"'on %s' % object.program_id.discount_specific_product_ids.name or ''\">Customizable Desk</strong>\n"
" </t>\n"
" </t>\n"
" <t t-elif=\"object.program_id.discount_apply_on == 'cheapest_product'\">\n"
" <br/><strong style=\"font-size: 24px;\">\n"
" off on the cheapest product\n"
" </strong>\n"
" </t>\n"
" <t t-else=\"\">\n"
" <br/><strong style=\"font-size: 24px;\">\n"
" off on your next order\n"
" </strong>\n"
" </t>\n"
" <br/>\n"
" </t>\n"
" <t t-elif=\"object.program_id.reward_type == 'product'\">\n"
" <span style=\"font-size: 36px; color: #875A7B; font-weight: bold;\" t-out=\"'get %s free %s' % (object.program_id.reward_product_quantity, object.program_id.reward_product_id.name) or ''\">get 1 free Corner Desk Right Sit</span><br/>\n"
" <strong style=\"font-size: 24px;\">on your next order</strong><br/>\n"
" </t>\n"
" <t t-elif=\"object.program_id.reward_type == 'free_shipping'\">\n"
" <span style=\"font-size: 36px; color: #875A7B; font-weight: bold;\">\n"
" get free shipping\n"
" </span><br/>\n"
" <strong style=\"font-size: 24px;\">on your next order</strong><br/>\n"
" </t>\n"
" </td></tr>\n"
" <tr style=\"margin-top: 16px\"><td valign=\"top\" style=\"text-align: center; font-size: 14px;\">\n"
" Use this promo code\n"
" <t t-if=\"object.expiration_date\">\n"
" before <t t-out=\"object.expiration_date or ''\">2021-06-16</t>\n"
" </t>\n"
" <p style=\"margin-top: 16px;\">\n"
" <strong style=\"padding: 16px 8px 16px 8px; border-radius: 3px; background-color: #F1F1F1;\" t-out=\"object.code or ''\">15637502648479132902</strong>\n"
" </p>\n"
" <t t-if=\"object.program_id.rule_min_quantity not in [0, 1]\">\n"
" <span style=\"font-size: 14px;\">\n"
" Minimum purchase of <t t-out=\"object.program_id.rule_min_quantity or ''\">10</t> products\n"
" </span><br/>\n"
" </t>\n"
" <t t-if=\"object.program_id.rule_minimum_amount != 0.00\">\n"
" <span style=\"font-size: 14px;\">\n"
" Valid for purchase above <t t-out=\"object.program_id.company_id.currency_id.symbol or ''\">$</t><t t-out=\"'%0.2f' % float(object.program_id.rule_minimum_amount) or ''\">10.00</t>\n"
" </span><br/>\n"
" </t>\n"
" <t t-if=\"display_specific_products\">\n"
" <span>\n"
" *Valid for following products: <t t-out=\"', '.join(object.program_id.discount_specific_product_ids.mapped('name')) or ''\">Customizable Desk</t>\n"
" </span><br/>\n"
" </t>\n"
" <br/>\n"
" Thank you,\n"
" <t t-if=\"object.order_id.user_id.signature\">\n"
" <br/>\n"
" <t t-out=\"object.order_id.user_id.signature or ''\">--<br/>Mitchell Admin</t>\n"
" </t>\n"
" </td></tr>\n"
"</tbody></table>\n"
" "
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#, python-format
msgid "A Coupon is already applied for the same reward"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid ""
"A minimum of %(amount)s %(currency)s should be purchased to get the reward"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order__applied_coupon_ids
msgid "Applied Coupons"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order__no_code_promo_program_ids
msgid "Applied Immediate Promo Programs"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order__code_promo_program_id
msgid "Applied Promo Program"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,help:sale_coupon.field_sale_order__promo_code
msgid "Applied program code"
msgstr ""
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_coupon_apply_code_view_form
msgid "Apply"
msgstr ""
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_coupon_apply_code_view_form
msgid "Apply coupon"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "At least one of the required conditions is not met to get the reward!"
msgstr ""
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_coupon_apply_code_view_form
msgid "Cancel"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__coupon_code
msgid "Code"
msgstr ""
#. module: sale_coupon
#: model:ir.model,name:sale_coupon.model_coupon_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_order_view_form
msgid "Coupon"
msgstr ""
#. module: sale_coupon
#: model:ir.model,name:sale_coupon.model_coupon_program
msgid "Coupon Program"
msgstr ""
#. module: sale_coupon
#: model:ir.ui.menu,name:sale_coupon.menu_coupon_type_config
#: model_terms:ir.ui.view,arch_db:sale_coupon.res_config_settings_view_form
msgid "Coupon Programs"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__create_uid
msgid "Created by"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__create_date
msgid "Created on"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/sale_order.py:0
#, python-format
msgid "Discount: %(program)s - On product with following taxes: %(taxes)s"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/sale_order.py:0
#: code:addons/sale_coupon/models/sale_order.py:0
#, python-format
msgid "Discount: %s"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__display_name
msgid "Display Name"
msgstr ""
#. module: sale_coupon
#: model:ir.actions.act_window,name:sale_coupon.sale_coupon_apply_code_action
msgid "Enter Promotion or Coupon Code"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/sale_order.py:0
#, python-format
msgid "Free Product"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#, python-format
msgid "Global discounts are not cumulable."
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "Global discounts are not cumulative."
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__id
msgid "ID"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order_line__is_reward_line
msgid "Is a program reward line"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code____last_update
msgid "Last Modified on"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__write_uid
msgid "Last Updated by"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_coupon_apply_code__write_date
msgid "Last Updated on"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order__generated_coupon_ids
msgid "Offered Coupons"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_coupon_program__order_count
msgid "Order Count"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_coupon_coupon__order_id
msgid "Order Reference"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "Promo code %s has been expired."
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "Promo code is expired"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "Promo code is invalid"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order__promo_code
msgid "Promotion Code"
msgstr ""
#. module: sale_coupon
#: model:ir.ui.menu,name:sale_coupon.menu_promotion_type_config
#: model_terms:ir.ui.view,arch_db:sale_coupon.res_config_settings_view_form
msgid "Promotion Programs"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "Promotionals codes are not cumulative."
msgstr ""
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_order_view_form
msgid "Promotions"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_sale_order__reward_amount
msgid "Reward Amount"
msgstr ""
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_coupon_program_view_coupon_program_form
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_coupon_program_view_promo_program_form
msgid "Sales"
msgstr ""
#. module: sale_coupon
#: model:ir.model,name:sale_coupon.model_sale_coupon_apply_code
msgid "Sales Coupon Apply Code"
msgstr ""
#. module: sale_coupon
#: model:ir.actions.act_window,name:sale_coupon.sale_order_action
#: model:ir.model,name:sale_coupon.model_sale_order
msgid "Sales Order"
msgstr ""
#. module: sale_coupon
#: model:ir.model,name:sale_coupon.model_sale_order_line
msgid "Sales Order Line"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "Sales Orders"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "The customer doesn't have access to this reward."
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "The promo code is already applied on this order"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "The promotional offer is already applied on this order"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid ""
"The reward products should be in the sales order lines to apply the "
"discount."
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,help:sale_coupon.field_coupon_coupon__order_id
msgid "The sales order from which coupon is generated"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,help:sale_coupon.field_coupon_coupon__sales_order_id
msgid "The sales order on which the coupon is applied"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/wizard/sale_coupon_apply_code.py:0
#, python-format
msgid "This coupon is invalid (%s)."
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid "This coupon is not yet usable. It will be starting from %s"
msgstr ""
#. module: sale_coupon
#: model:ir.model.fields,field_description:sale_coupon.field_coupon_coupon__sales_order_id
msgid "Used in"
msgstr ""
#. module: sale_coupon
#: model_terms:ir.ui.view,arch_db:sale_coupon.sale_order_view_form
msgid ""
"When clicked, the content of the order will be checked to detect (and apply)"
" possible promotion programs."
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon.py:0
#, python-format
msgid ""
"You don't have the required product quantities on your sales order. All the "
"products should be recorded on the sales order. (Example: You need to have 3"
" T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free')."
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/coupon_program.py:0
#, python-format
msgid ""
"You don't have the required product quantities on your sales order. If the "
"reward is same product quantity, please make sure that all the products are "
"recorded on the sales order (Example: You need to have 3 T-shirts on your "
"sales order if the promotion is 'Buy 2, Get 1 Free'."
msgstr ""
#. module: sale_coupon
#: model:mail.template,report_name:sale_coupon.mail_template_sale_coupon
msgid "Your Coupon Code"
msgstr ""
#. module: sale_coupon
#: model:mail.template,subject:sale_coupon.mail_template_sale_coupon
msgid "Your reward coupon from {{ object.program_id.company_id.name }} "
msgstr ""
#. module: sale_coupon
#: model:mail.template,name:sale_coupon.mail_template_sale_coupon
msgid "[Sales] Coupon: Send by Email"
msgstr ""
#. module: sale_coupon
#: code:addons/sale_coupon/models/sale_order.py:0
#, python-format
msgid "limited to "
msgstr ""
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-7
View File
@@ -1,7 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import coupon
from . import coupon_program
from . import sale_order
from . import sale_order_line
-49
View File
@@ -1,49 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import api, fields, models, _
class Coupon(models.Model):
_inherit = 'coupon.coupon'
order_id = fields.Many2one('sale.order', 'Order Reference', readonly=True,
help="The sales order from which coupon is generated")
sales_order_id = fields.Many2one('sale.order', 'Used in', readonly=True,
help="The sales order on which the coupon is applied")
def _check_coupon_code(self, order_date, partner_id, **kwargs):
message = super(Coupon, self)._check_coupon_code(order_date, partner_id, **kwargs)
order = kwargs.get('order', False)
if message.get('error', False) or not order:
return message
applicable_programs = order._get_applicable_programs()
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if self.program_id.program_type == 'coupon_program' and not self.program_id._filter_on_mimimum_amount(order):
message = {'error': _(
'A minimum of %(amount)s %(currency)s should be purchased to get the reward',
amount=self.program_id.rule_minimum_amount,
currency=self.program_id.currency_id.name
)}
elif self.program_id in order.applied_coupon_ids.mapped('program_id'):
message = {'error': _('A Coupon is already applied for the same reward')}
elif self.program_id._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulable.')}
elif self.program_id.reward_type == 'product' and not order._is_reward_in_order_lines(self.program_id):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self.program_id._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
elif self.program_id.program_type == 'coupon_program' and not self.program_id._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. All the products should be recorded on the sales order. (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free').")}
else:
if self.program_id not in applicable_programs and self.program_id.promo_applicability == 'on_current_order':
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
def _get_default_template(self):
default_template = super()._get_default_template()
if not default_template:
return self.env.ref('sale_coupon.mail_template_sale_coupon', False)
return default_template
-214
View File
@@ -1,214 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import fields, models, _
from babel.dates import format_datetime
class CouponProgram(models.Model):
_inherit = 'coupon.program'
order_count = fields.Integer(compute='_compute_order_count')
# The api.depends is handled in `def modified` of `sale_coupon/models/sale_order.py`
def _compute_order_count(self):
for program in self:
program.order_count = self.env['sale.order.line'].sudo().search_count([('product_id', '=', program.discount_line_product_id.id)])
def action_view_sales_orders(self):
self.ensure_one()
orders = self.env['sale.order.line'].search([('product_id', '=', self.discount_line_product_id.id)]).mapped('order_id')
return {
'name': _('Sales Orders'),
'view_mode': 'tree,form',
'res_model': 'sale.order',
'search_view_id': [self.env.ref('sale.sale_order_view_search_inherit_quotation').id],
'type': 'ir.actions.act_window',
'domain': [('id', 'in', orders.ids)],
'context': dict(self._context, create=False),
}
def _check_promo_code(self, order, coupon_code):
message = {}
if self.maximum_use_number != 0 and self.total_order_count >= self.maximum_use_number:
message = {'error': _('Promo code %s has been expired.') % (coupon_code)}
elif not self._filter_on_mimimum_amount(order):
message = {'error': _(
'A minimum of %(amount)s %(currency)s should be purchased to get the reward',
amount=self.rule_minimum_amount,
currency=self.currency_id.name
)}
elif self.promo_code and self.promo_code == order.promo_code:
message = {'error': _('The promo code is already applied on this order')}
elif self in order.no_code_promo_program_ids:
message = {'error': _('The promotional offer is already applied on this order')}
elif not self.active:
message = {'error': _('Promo code is invalid')}
elif self.rule_date_from and self.rule_date_from > fields.Datetime.now():
tzinfo = self.env.context.get('tz') or self.env.user.tz or 'UTC'
locale = self.env.context.get('lang') or self.env.user.lang or 'en_US'
message = {'error': _('This coupon is not yet usable. It will be starting from %s') % (format_datetime(self.rule_date_from, format='short', tzinfo=tzinfo, locale=locale))}
elif self.rule_date_to and fields.Datetime.now() > self.rule_date_to:
message = {'error': _('Promo code is expired')}
elif order.promo_code and self.promo_code_usage == 'code_needed':
message = {'error': _('Promotionals codes are not cumulative.')}
elif self._is_global_discount_program() and order._is_global_discount_already_applied():
message = {'error': _('Global discounts are not cumulative.')}
elif self.promo_applicability == 'on_current_order' and self.reward_type == 'product' and not order._is_reward_in_order_lines(self):
message = {'error': _('The reward products should be in the sales order lines to apply the discount.')}
elif not self._is_valid_partner(order.partner_id):
message = {'error': _("The customer doesn't have access to this reward.")}
elif not self._filter_programs_on_products(order):
message = {'error': _("You don't have the required product quantities on your sales order. If the reward is same product quantity, please make sure that all the products are recorded on the sales order (Example: You need to have 3 T-shirts on your sales order if the promotion is 'Buy 2, Get 1 Free'.")}
elif self.promo_applicability == 'on_current_order' and not self.env.context.get('applicable_coupon'):
applicable_programs = order._get_applicable_programs()
if self not in applicable_programs:
message = {'error': _('At least one of the required conditions is not met to get the reward!')}
return message
def _filter_on_mimimum_amount(self, order):
no_effect_lines = order._get_no_effect_on_threshold_lines()
order_amount = {
'amount_untaxed' : order.amount_untaxed - sum(line.price_subtotal for line in no_effect_lines),
'amount_tax' : order.amount_tax - sum(line.price_tax for line in no_effect_lines)
}
program_ids = list()
for program in self:
if program.reward_type != 'discount':
# avoid the filtered
lines = self.env['sale.order.line']
else:
lines = order.order_line.filtered(lambda line:
line.product_id == program.discount_line_product_id or
line.product_id == program.reward_id.discount_line_product_id or
(program.program_type == 'promotion_program' and line.is_reward_line)
)
untaxed_amount = order_amount['amount_untaxed'] - sum(line.price_subtotal for line in lines)
tax_amount = order_amount['amount_tax'] - sum(line.price_tax for line in lines)
program_amount = program._compute_program_amount('rule_minimum_amount', order.currency_id)
if program.rule_minimum_amount_tax_inclusion == 'tax_included' and program_amount <= (untaxed_amount + tax_amount) or program_amount <= untaxed_amount:
program_ids.append(program.id)
return self.browse(program_ids)
def _filter_on_validity_dates(self, order):
return self.filtered(lambda program:
(not program.rule_date_from or program.rule_date_from <= fields.Datetime.now())
and
(not program.rule_date_to or program.rule_date_to >= fields.Datetime.now())
)
def _filter_promo_programs_with_code(self, order):
'''Filter Promo program with code with a different promo_code if a promo_code is already ordered'''
return self.filtered(lambda program: program.promo_code_usage == 'code_needed' and program.promo_code != order.promo_code)
def _filter_unexpired_programs(self, order):
return self.filtered(
lambda program: program.maximum_use_number == 0
or program.total_order_count < program.maximum_use_number
or program
in (order.code_promo_program_id + order.no_code_promo_program_ids)
)
def _filter_programs_on_partners(self, order):
return self.filtered(lambda program: program._is_valid_partner(order.partner_id))
def _filter_programs_on_products(self, order):
"""
To get valid programs according to product list.
i.e Buy 1 imac + get 1 ipad mini free then check 1 imac is on cart or not
or Buy 1 coke + get 1 coke free then check 2 cokes are on cart or not
"""
order_lines = order.order_line.filtered(lambda line: line.product_id) - order._get_reward_lines()
products = order_lines.mapped('product_id')
products_qties = dict.fromkeys(products, 0)
for line in order_lines:
products_qties[line.product_id] += line.product_uom_qty
valid_program_ids = list()
for program in self:
if not program.rule_products_domain or program.rule_products_domain == "[]":
valid_program_ids.append(program.id)
continue
valid_products = program._get_valid_products(products)
if not valid_products:
# The program can be directly discarded
continue
ordered_rule_products_qty = sum(products_qties[product] for product in valid_products)
# Avoid program if 1 ordered foo on a program '1 foo, 1 free foo'
if program.promo_applicability == 'on_current_order' and \
program.reward_type == 'product' and program._get_valid_products(program.reward_product_id):
ordered_rule_products_qty -= program.reward_product_quantity
if ordered_rule_products_qty >= program.rule_min_quantity:
valid_program_ids.append(program.id)
return self.browse(valid_program_ids)
def _filter_not_ordered_reward_programs(self, order):
"""
Returns the programs when the reward is actually in the order lines
"""
programs = self.env['coupon.program']
order_products = order.order_line.product_id
for program in self:
if program.reward_type == 'product' and program.reward_product_id not in order_products:
continue
elif (
program.reward_type == 'discount'
and program.discount_apply_on == 'specific_products'
and not any(discount_product in order_products for discount_product in program.discount_specific_product_ids)
):
continue
programs += program
return programs
def _filter_programs_from_common_rules(self, order, next_order=False):
""" Return the programs if every conditions is met
:param bool next_order: is the reward given from a previous order
"""
programs = self
# Minimum requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_on_mimimum_amount(order)
if not self.env.context.get("no_outdated_coupons"):
programs = programs and programs._filter_on_validity_dates(order)
programs = programs and programs._filter_unexpired_programs(order)
programs = programs and programs._filter_programs_on_partners(order)
# Product requirement should not be checked if the coupon got generated by a promotion program (the requirement should have only be checked to generate the coupon)
if not next_order:
programs = programs and programs._filter_programs_on_products(order)
programs_curr_order = programs.filtered(lambda p: p.promo_applicability == 'on_current_order')
programs = programs.filtered(lambda p: p.promo_applicability == 'on_next_order')
if programs_curr_order:
# Checking if rewards are in the SO should not be performed for rewards on_next_order
programs += programs_curr_order._filter_not_ordered_reward_programs(order)
return programs
def _get_discount_product_values(self):
res = super()._get_discount_product_values()
res['invoice_policy'] = 'order'
return res
def _is_global_discount_program(self):
self.ensure_one()
return self.promo_applicability == 'on_current_order' and \
self.reward_type == 'discount' and \
self.discount_type == 'percentage' and \
self.discount_apply_on == 'on_order'
def _keep_only_most_interesting_auto_applied_global_discount_program(self):
'''Given a record set of programs, remove the less interesting auto
applied global discount to keep only the most interesting one.
We should not take promo code programs into account as a 10% auto
applied is considered better than a 50% promo code, as the user might
not know about the promo code.
'''
programs = self.filtered(lambda p: p._is_global_discount_program() and p.promo_code_usage == 'no_code_needed')
if not programs: return self
most_interesting_program = max(programs, key=lambda p: p.discount_percentage)
# remove least interesting programs
return self - (programs - most_interesting_program)
# The api.depends is handled in `def modified` of `sale_coupon/models/sale_order.py`
def _compute_total_order_count(self):
super(CouponProgram, self)._compute_total_order_count()
for program in self:
program.total_order_count += program.order_count
-547
View File
@@ -1,547 +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.tools.misc import formatLang
class SaleOrder(models.Model):
_inherit = "sale.order"
applied_coupon_ids = fields.One2many('coupon.coupon', 'sales_order_id', string="Applied Coupons", copy=False)
generated_coupon_ids = fields.One2many('coupon.coupon', 'order_id', string="Offered Coupons", copy=False)
reward_amount = fields.Float(compute='_compute_reward_total')
no_code_promo_program_ids = fields.Many2many('coupon.program', string="Applied Immediate Promo Programs",
domain="[('promo_code_usage', '=', 'no_code_needed'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", copy=False)
code_promo_program_id = fields.Many2one('coupon.program', string="Applied Promo Program",
domain="[('promo_code_usage', '=', 'code_needed'), '|', ('company_id', '=', False), ('company_id', '=', company_id)]", copy=False)
promo_code = fields.Char(related='code_promo_program_id.promo_code', help="Applied program code", readonly=False)
@api.depends('order_line')
def _compute_reward_total(self):
for order in self:
order.reward_amount = sum([line.price_subtotal for line in order._get_reward_lines()])
def _get_no_effect_on_threshold_lines(self):
self.ensure_one()
lines = self.env['sale.order.line']
return lines
def recompute_coupon_lines(self):
for order in self:
order._remove_invalid_reward_lines()
if order.state != 'cancel':
order._create_new_no_code_promo_reward_lines()
order._update_existing_reward_lines()
@api.returns('self', lambda value: value.id)
def copy(self, default=None):
order = super(SaleOrder, self).copy(default)
reward_line = order._get_reward_lines()
if reward_line:
reward_line.unlink()
order._create_new_no_code_promo_reward_lines()
return order
def action_confirm(self):
self.generated_coupon_ids.write({'state': 'new'})
self.applied_coupon_ids.write({'state': 'used'})
self._send_reward_coupon_mail()
return super(SaleOrder, self).action_confirm()
def _action_cancel(self):
res = super()._action_cancel()
self.generated_coupon_ids.write({'state': 'expired'})
self.applied_coupon_ids.write({'state': 'new'})
self.applied_coupon_ids.sales_order_id = False
self.recompute_coupon_lines()
return res
def action_draft(self):
res = super(SaleOrder, self).action_draft()
self.generated_coupon_ids.write({'state': 'reserved'})
return res
def _get_reward_lines(self):
return self.order_line.filtered(lambda line: line.is_reward_line)
def _is_reward_in_order_lines(self, program):
self.ensure_one()
order_quantity = sum(self.order_line.filtered(lambda line:
line.product_id == program.reward_product_id).mapped('product_uom_qty'))
return order_quantity >= program.reward_product_quantity
def _is_global_discount_already_applied(self):
applied_programs = self.no_code_promo_program_ids + \
self.code_promo_program_id + \
self.applied_coupon_ids.mapped('program_id')
return applied_programs.filtered(lambda program: program._is_global_discount_program())
def _get_reward_values_product(self, program):
price_unit = self.order_line.filtered(lambda line: program.reward_product_id == line.product_id)[0].price_reduce
order_lines = (self.order_line - self._get_reward_lines()).filtered(lambda x: program._get_valid_products(x.product_id))
max_product_qty = sum(order_lines.mapped('product_uom_qty')) or 1
total_qty = sum(self.order_line.filtered(lambda x: x.product_id == program.reward_product_id).mapped('product_uom_qty'))
# Remove needed quantity from reward quantity if same reward and rule product
if program._get_valid_products(program.reward_product_id):
# number of times the program should be applied
program_in_order = max_product_qty // (program.rule_min_quantity + program.reward_product_quantity)
# multipled by the reward qty
reward_product_qty = program.reward_product_quantity * program_in_order
# do not give more free reward than products
reward_product_qty = min(reward_product_qty, total_qty)
if program.rule_minimum_amount:
order_total = sum(order_lines.mapped('price_total')) - (program.reward_product_quantity * program.reward_product_id.lst_price)
reward_product_qty = min(reward_product_qty, order_total // program.rule_minimum_amount)
else:
program_in_order = max_product_qty // program.rule_min_quantity
reward_product_qty = min(program.reward_product_quantity * program_in_order, total_qty)
reward_qty = min(int(int(max_product_qty / program.rule_min_quantity) * program.reward_product_quantity), reward_product_qty)
# Take the default taxes on the reward product, mapped with the fiscal position
taxes = program.reward_product_id.taxes_id.filtered(lambda t: t.company_id.id == self.company_id.id)
taxes = self.fiscal_position_id.map_tax(taxes)
return {
'product_id': program.discount_line_product_id.id,
'price_unit': - price_unit,
'product_uom_qty': reward_qty,
'is_reward_line': True,
'name': _("Free Product") + " - " + program.reward_product_id.name,
'product_uom': program.reward_product_id.uom_id.id,
'tax_id': [(4, tax.id, False) for tax in taxes],
}
def _get_paid_order_lines(self):
""" Returns the sale order lines that are not reward lines.
It will also return reward lines being free product lines. """
free_reward_product = self.env['coupon.program'].search([('reward_type', '=', 'product')]).mapped('discount_line_product_id')
return self.order_line.filtered(lambda x: not x._is_not_sellable_line() or x.product_id in free_reward_product)
def _get_base_order_lines(self, program):
""" Returns the sale order lines not linked to the given program.
"""
return self.order_line.filtered(lambda x: not x._is_not_sellable_line() or (x.is_reward_line and x.product_id != program.discount_line_product_id))
def _get_reward_values_discount_fixed_amount(self, program):
total_amount = sum(self._get_base_order_lines(program).mapped('price_total'))
fixed_amount = program._compute_program_amount('discount_fixed_amount', self.currency_id)
if total_amount < fixed_amount:
return total_amount
else:
return fixed_amount
def _get_cheapest_line(self):
# Unit prices tax included
return min(self.order_line.filtered(lambda x: not x._is_not_sellable_line() and x.price_reduce > 0), key=lambda x: x['price_reduce'])
def _get_reward_values_discount_percentage_per_line(self, program, line):
discount_amount = line.product_uom_qty * line.price_reduce * (program.discount_percentage / 100)
return discount_amount
def _get_reward_values_discount(self, program):
if program.discount_type == 'fixed_amount':
product_taxes = program.discount_line_product_id.taxes_id.filtered(lambda tax: tax.company_id == self.company_id)
taxes = self.fiscal_position_id.map_tax(product_taxes)
return [{
'name': _("Discount: %s", program.name),
'product_id': program.discount_line_product_id.id,
'price_unit': - self._get_reward_values_discount_fixed_amount(program),
'product_uom_qty': 1.0,
'product_uom': program.discount_line_product_id.uom_id.id,
'is_reward_line': True,
'tax_id': [(4, tax.id, False) for tax in taxes],
}]
reward_dict = {}
lines = self._get_paid_order_lines()
amount_total = sum(self._get_base_order_lines(program).mapped('price_subtotal'))
if program.discount_apply_on == 'cheapest_product':
line = self._get_cheapest_line()
if line:
discount_line_amount = min(line.price_reduce * (program.discount_percentage / 100), amount_total)
if discount_line_amount:
taxes = self.fiscal_position_id.map_tax(line.tax_id)
reward_dict[line.tax_id] = {
'name': _("Discount: %s", program.name),
'product_id': program.discount_line_product_id.id,
'price_unit': - discount_line_amount if discount_line_amount > 0 else 0,
'product_uom_qty': 1.0,
'product_uom': program.discount_line_product_id.uom_id.id,
'is_reward_line': True,
'tax_id': [(4, tax.id, False) for tax in taxes],
}
elif program.discount_apply_on in ['specific_products', 'on_order']:
if program.discount_apply_on == 'specific_products':
# We should not exclude reward line that offer this product since we need to offer only the discount on the real paid product (regular product - free product)
free_product_lines = self.env['coupon.program'].search([('reward_type', '=', 'product'), ('reward_product_id', 'in', program.discount_specific_product_ids.ids)]).mapped('discount_line_product_id')
lines = lines.filtered(lambda x: x.product_id in (program.discount_specific_product_ids | free_product_lines))
# when processing lines we should not discount more than the order remaining total
currently_discounted_amount = 0
for line in lines:
discount_line_amount = min(self._get_reward_values_discount_percentage_per_line(program, line), amount_total - currently_discounted_amount)
if discount_line_amount:
if line.tax_id in reward_dict:
reward_dict[line.tax_id]['price_unit'] -= discount_line_amount
else:
taxes = self.fiscal_position_id.map_tax(line.tax_id)
reward_dict[line.tax_id] = {
'name': _(
"Discount: %(program)s - On product with following taxes: %(taxes)s",
program=program.name,
taxes=", ".join(taxes.mapped('name')),
),
'product_id': program.discount_line_product_id.id,
'price_unit': - discount_line_amount if discount_line_amount > 0 else 0,
'product_uom_qty': 1.0,
'product_uom': program.discount_line_product_id.uom_id.id,
'is_reward_line': True,
'tax_id': [(4, tax.id, False) for tax in taxes],
}
currently_discounted_amount += discount_line_amount
# If there is a max amount for discount, we might have to limit some discount lines or completely remove some lines
max_amount = program._compute_program_amount('discount_max_amount', self.currency_id)
if max_amount > 0:
amount_already_given = 0
for val in list(reward_dict):
amount_to_discount = amount_already_given + reward_dict[val]["price_unit"]
if abs(amount_to_discount) > max_amount:
reward_dict[val]["price_unit"] = - (max_amount - abs(amount_already_given))
add_name = formatLang(self.env, max_amount, currency_obj=self.currency_id)
reward_dict[val]["name"] += "( " + _("limited to ") + add_name + ")"
amount_already_given += reward_dict[val]["price_unit"]
if reward_dict[val]["price_unit"] == 0:
del reward_dict[val]
return reward_dict.values()
def _get_reward_line_values(self, program):
self.ensure_one()
self = self.with_context(lang=self.partner_id.lang)
program = program.with_context(lang=self.partner_id.lang)
if program.reward_type == 'discount':
return self._get_reward_values_discount(program)
elif program.reward_type == 'product':
return [self._get_reward_values_product(program)]
def _create_reward_line(self, program):
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
def _create_reward_coupon(self, program):
# if there is already a coupon that was set as expired, reactivate that one instead of creating a new one
coupon = self.env['coupon.coupon'].search([
('program_id', '=', program.id),
('state', '=', 'expired'),
('partner_id', '=', self.partner_id.id),
('order_id', '=', self.id),
('discount_line_product_id', '=', program.discount_line_product_id.id),
], limit=1)
if coupon:
coupon.write({'state': 'reserved'})
else:
coupon = self.env['coupon.coupon'].sudo().create({
'program_id': program.id,
'state': 'reserved',
'partner_id': self.partner_id.id,
'order_id': self.id,
'discount_line_product_id': program.discount_line_product_id.id
})
self.generated_coupon_ids |= coupon
return coupon
def _send_reward_coupon_mail(self):
template = self.env.ref('sale_coupon.mail_template_sale_coupon', raise_if_not_found=False)
if template:
for order in self:
for coupon in order.generated_coupon_ids:
order.message_post_with_template(
template.id, composition_mode='comment',
model='coupon.coupon', res_id=coupon.id,
email_layout_xmlid='mail.mail_notification_light',
)
def _get_applicable_programs(self):
"""
This method is used to return the valid applicable programs on given order.
"""
self.ensure_one()
programs = self.env['coupon.program'].with_context(
no_outdated_coupons=True,
).search([
('company_id', 'in', [self.company_id.id, False]),
'|', ('rule_date_from', '=', False), ('rule_date_from', '<=', fields.Datetime.now()),
'|', ('rule_date_to', '=', False), ('rule_date_to', '>=', fields.Datetime.now()),
], order="id")._filter_programs_from_common_rules(self)
# no impact code...
# should be programs = programs.filtered if we really want to filter...
# if self.promo_code:
# programs._filter_promo_programs_with_code(self)
return programs
def _get_applicable_no_code_promo_program(self):
self.ensure_one()
programs = self.env['coupon.program'].with_context(
no_outdated_coupons=True,
applicable_coupon=True,
).search([
('promo_code_usage', '=', 'no_code_needed'),
'|', ('rule_date_from', '=', False), ('rule_date_from', '<=', fields.Datetime.now()),
'|', ('rule_date_to', '=', False), ('rule_date_to', '>=', fields.Datetime.now()),
'|', ('company_id', '=', self.company_id.id), ('company_id', '=', False),
])._filter_programs_from_common_rules(self)
return programs
def _get_valid_applied_coupon_program(self):
self.ensure_one()
# applied_coupon_ids's coupons might be coming from:
# * a coupon generated from a previous order that benefited from a promotion_program that rewarded the next sale order.
# In that case requirements to benefit from the program (Quantity and price) should not be checked anymore
# * a coupon_program, in that case the promo_applicability is always for the current order and everything should be checked (filtered)
programs = self.applied_coupon_ids.mapped('program_id').filtered(lambda p: p.promo_applicability == 'on_next_order')._filter_programs_from_common_rules(self, True)
programs += self.applied_coupon_ids.mapped('program_id').filtered(lambda p: p.promo_applicability == 'on_current_order')._filter_programs_from_common_rules(self)
return programs
def _create_new_no_code_promo_reward_lines(self):
'''Apply new programs that are applicable'''
self.ensure_one()
order = self
programs = order._get_applicable_no_code_promo_program()
programs = programs._keep_only_most_interesting_auto_applied_global_discount_program()
for program in programs:
# VFE REF in master _get_applicable_no_code_programs already filters programs
# why do we need to reapply this bunch of checks in _check_promo_code ????
# We should only apply a little part of the checks in _check_promo_code...
error_status = program._check_promo_code(order, False)
if not error_status.get('error'):
if program.promo_applicability == 'on_next_order':
order.state != 'cancel' and order._create_reward_coupon(program)
elif program.discount_line_product_id.id not in self.order_line.mapped('product_id').ids:
self.write({'order_line': [(0, False, value) for value in self._get_reward_line_values(program)]})
order.no_code_promo_program_ids |= program
def _update_existing_reward_lines(self):
'''Update values for already applied rewards'''
def update_line(order, lines, values):
'''Update the lines and return them if they should be deleted'''
lines_to_remove = self.env['sale.order.line']
# Check commit 6bb42904a03 for next if/else
# Remove reward line if price or qty equal to 0
if values['product_uom_qty'] and values['price_unit']:
lines.write(values)
else:
if program.reward_type != 'free_shipping':
# Can't remove the lines directly as we might be in a recordset loop
lines_to_remove += lines
else:
values.update(price_unit=0.0)
lines.write(values)
return lines_to_remove
self.ensure_one()
order = self
applied_programs = order._get_applied_programs_with_rewards_on_current_order()
for program in applied_programs:
values = order._get_reward_line_values(program)
lines = order.order_line.filtered(lambda line: line.product_id == program.discount_line_product_id)
if program.reward_type == 'discount' and program.discount_type == 'percentage':
lines_to_remove = lines
# Values is what discount lines should really be, lines is what we got in the SO at the moment
# 1. If values & lines match, we should update the line (or delete it if no qty or price?)
# 2. If the value is not in the lines, we should add it
# 3. if the lines contains a tax not in value, we should remove it
for value in values:
value_found = False
for line in lines:
# Case 1.
if not len(set(line.tax_id.mapped('id')).symmetric_difference(set([v[1] for v in value['tax_id']]))):
value_found = True
# Working on Case 3.
lines_to_remove -= line
lines_to_remove += update_line(order, line, value)
continue
# Case 2.
if not value_found:
order.write({'order_line': [(0, False, value)]})
# Case 3.
lines_to_remove.unlink()
else:
update_line(order, lines, values[0]).unlink()
def _remove_invalid_reward_lines(self):
""" Find programs & coupons that are not applicable anymore.
It will then unlink the related reward order lines.
It will also unset the order's fields that are storing
the applied coupons & programs.
Note: It will also remove a reward line coming from an archive program.
"""
self.ensure_one()
order = self
applied_programs = order._get_applied_programs()
applicable_programs = self.env['coupon.program']
if applied_programs:
applicable_programs = order._get_applicable_programs() + order._get_valid_applied_coupon_program()
applicable_programs = applicable_programs._keep_only_most_interesting_auto_applied_global_discount_program()
programs_to_remove = applied_programs - applicable_programs
reward_product_ids = applied_programs.discount_line_product_id.ids
# delete reward line coming from an archived coupon (it will never be updated/removed when recomputing the order)
invalid_lines = order.order_line.filtered(lambda line: line.is_reward_line and line.product_id.id not in reward_product_ids)
# Remove coupons that are now expired
expired_coupons = order.applied_coupon_ids.filtered(
lambda coupon: coupon.expiration_date and coupon.expiration_date < fields.Date.today())
programs_to_remove |= expired_coupons.program_id
if programs_to_remove:
product_ids_to_remove = programs_to_remove.discount_line_product_id.ids
if product_ids_to_remove:
# Invalid generated coupon for which we are not eligible anymore ('expired' since it is specific to this SO and we may again met the requirements)
self.generated_coupon_ids.filtered(lambda coupon: coupon.program_id.discount_line_product_id.id in product_ids_to_remove).write({'state': 'expired'})
# Reset applied coupons for which we are not eligible anymore ('valid' so it can be use on another )
coupons_to_remove = order.applied_coupon_ids.filtered(lambda coupon: coupon.program_id in programs_to_remove)
coupons_to_remove.write({'state': 'new'})
# Unbind promotion and coupon programs which requirements are not met anymore
order.no_code_promo_program_ids -= programs_to_remove
order.code_promo_program_id -= programs_to_remove
if coupons_to_remove:
order.applied_coupon_ids -= coupons_to_remove
# Remove their reward lines
if product_ids_to_remove:
invalid_lines |= order.order_line.filtered(lambda line: line.product_id.id in product_ids_to_remove)
invalid_lines.unlink()
def _get_applied_programs_with_rewards_on_current_order(self):
# Need to add filter on current order. Indeed, it has always been calculating reward line even if on next order (which is useless and do calculation for nothing)
# This problem could not be noticed since it would only update or delete existing lines related to that program, it would not find the line to update since not in the order
# But now if we dont find the reward line in the order, we add it (since we can now have multiple line per program in case of discount on different vat), thus the bug
# mentionned ahead will be seen now
return self.no_code_promo_program_ids.filtered(lambda p: p.promo_applicability == 'on_current_order') + \
self.applied_coupon_ids.mapped('program_id') + \
self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_current_order')
def _get_applied_programs_with_rewards_on_next_order(self):
return self.no_code_promo_program_ids.filtered(lambda p: p.promo_applicability == 'on_next_order') + \
self.code_promo_program_id.filtered(lambda p: p.promo_applicability == 'on_next_order')
def _get_applied_programs(self):
"""Returns all applied programs on current order:
Expected to return same result than:
self._get_applied_programs_with_rewards_on_current_order()
+
self._get_applied_programs_with_rewards_on_next_order()
"""
return self.code_promo_program_id + self.no_code_promo_program_ids + self.applied_coupon_ids.mapped('program_id')
def _get_invoice_status(self):
# Handling of a specific situation: an order contains
# a product invoiced on delivery and a promo line invoiced
# on order. We would avoid having the invoice status 'to_invoice'
# if the created invoice will only contain the promotion line
super()._get_invoice_status()
for order in self.filtered(lambda order: order.invoice_status == 'to invoice'):
paid_lines = order._get_paid_order_lines()
if not any(line.invoice_status == 'to invoice' for line in paid_lines):
order.invoice_status = 'no'
def _get_invoiceable_lines(self, final=False):
""" Ensures we cannot invoice only reward lines.
Since promotion lines are specified with service products,
those lines are directly invoiceable when the order is confirmed
which can result in invoices containing only promotion lines.
To avoid those cases, we allow the invoicing of promotion lines
iff at least another 'basic' lines is also invoiceable.
"""
invoiceable_lines = super()._get_invoiceable_lines(final)
reward_lines = self._get_reward_lines()
if invoiceable_lines <= reward_lines:
return self.env['sale.order.line'].browse()
return invoiceable_lines
def update_prices(self):
"""Recompute coupons/promotions after pricelist prices reset."""
super().update_prices()
if any(line.is_reward_line for line in self.order_line):
self.recompute_coupon_lines()
class SaleOrderLine(models.Model):
_inherit = "sale.order.line"
is_reward_line = fields.Boolean('Is a program reward line')
def _is_not_sellable_line(self):
return self.is_reward_line or super()._is_not_sellable_line()
def unlink(self):
related_program_lines = self.env['sale.order.line']
# Reactivate coupons related to unlinked reward line
for line in self.filtered(lambda line: line.is_reward_line):
coupons_to_reactivate = line.order_id.applied_coupon_ids.filtered(
lambda coupon: coupon.program_id.discount_line_product_id == line.product_id
)
coupons_to_reactivate.write({'state': 'new'})
line.order_id.applied_coupon_ids -= coupons_to_reactivate
# Remove the program from the order if the deleted line is the reward line of the program
# And delete the other lines from this program (It's the case when discount is split per different taxes)
related_program = self.env['coupon.program'].search([('discount_line_product_id', '=', line.product_id.id)])
if related_program:
line.order_id.no_code_promo_program_ids -= related_program
line.order_id.code_promo_program_id -= related_program
related_program_lines |= line.order_id.order_line.filtered(lambda l: l.product_id.id == related_program.discount_line_product_id.id) - line
return super(SaleOrderLine, self | related_program_lines).unlink()
def _compute_tax_id(self):
reward_lines = self.filtered('is_reward_line')
super(SaleOrderLine, self - reward_lines)._compute_tax_id()
# Discount reward line is split per tax, the discount is set on the line but not on the product
# as the product is the generic discount line.
# In case of a free product, retrieving the tax on the line instead of the product won't affect the behavior.
for line in reward_lines:
line = line.with_company(line.company_id)
fpos = line.order_id.fiscal_position_id or line.order_id.fiscal_position_id._get_fiscal_position(line.order_partner_id)
# If company_id is set, always filter taxes by the company
taxes = line.tax_id.filtered(lambda r: not line.company_id or r.company_id == line.company_id)
line.tax_id = fpos.map_tax(taxes)
def _get_display_price(self):
# A product created from a promotion does not have a list_price.
# The price_unit of a reward order line is computed by the promotion, so it can be used directly
if self.is_reward_line:
return self.price_unit
return super()._get_display_price()
# Invalidation of `coupon.program.order_count`
# `test_program_rules_validity_dates_and_uses`,
# Overriding modified is quite hardcore as you need to know how works the cache and the invalidation system,
# but at least the below works and should be efficient.
# Another possibility is to add on product.product a one2many to sale.order.line 'order_line_ids',
# and then add the depends @api.depends('discount_line_product_id.order_line_ids'),
# but I am not sure this will as efficient as the below.
def modified(self, fnames, *args, **kwargs):
super(SaleOrderLine, self).modified(fnames, *args, **kwargs)
if 'product_id' in fnames:
Program = self.env['coupon.program'].sudo()
field_order_count = Program._fields['order_count']
field_total_order_count = Program._fields['total_order_count']
programs = self.env.cache.get_records(Program, field_order_count)
programs |= self.env.cache.get_records(Program, field_total_order_count)
if programs:
products = self.filtered('is_reward_line').mapped('product_id')
for program in programs:
if program.discount_line_product_id in products:
self.env.cache.invalidate([(field_order_count, program.ids), (field_total_order_count, program.ids)])
@@ -1,12 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models
class OrderLine(models.Model):
_inherit = 'sale.order.line'
def _compute_name(self):
# Avoid computing the name for reward lines
reward = self.filtered('is_reward_line')
super(OrderLine, self - reward)._compute_name()
@@ -1,11 +0,0 @@
id,name,model_id/id,group_id/id,perm_read,perm_write,perm_create,perm_unlink
access_program_salesman,Coupon Program (Salesperson),coupon.model_coupon_program,sales_team.group_sale_salesman,1,0,0,0
access_program_manager,Coupon Program (Manager),coupon.model_coupon_program,sales_team.group_sale_manager,1,1,1,1
access_applicability_salesman,Coupon Rule (Salesperson),coupon.model_coupon_rule,sales_team.group_sale_salesman,1,0,0,0
access_applicability_manager,Coupon Rule (Manager),coupon.model_coupon_rule,sales_team.group_sale_manager,1,1,1,0
access_coupon_salesman,Coupon (Salesperson),coupon.model_coupon_coupon,sales_team.group_sale_salesman,1,1,0,0
access_coupon_manager,Coupon (Manager),coupon.model_coupon_coupon,sales_team.group_sale_manager,1,1,1,0
access_reward_salesman,Coupon Reward (Salesperson),coupon.model_coupon_reward,sales_team.group_sale_salesman,1,0,0,0
access_reward_manager,Coupon Reward (Manager),coupon.model_coupon_reward,sales_team.group_sale_manager,1,1,1,0
access_sale_coupon_apply_code,Apply Coupon,model_sale_coupon_apply_code,sales_team.group_sale_salesman,1,1,1,0
access_sale_coupon_generate,Coupon Generation,coupon.model_coupon_generate_wizard,sales_team.group_sale_salesman,1,1,1,0
1 id name model_id/id group_id/id perm_read perm_write perm_create perm_unlink
2 access_program_salesman Coupon Program (Salesperson) coupon.model_coupon_program sales_team.group_sale_salesman 1 0 0 0
3 access_program_manager Coupon Program (Manager) coupon.model_coupon_program sales_team.group_sale_manager 1 1 1 1
4 access_applicability_salesman Coupon Rule (Salesperson) coupon.model_coupon_rule sales_team.group_sale_salesman 1 0 0 0
5 access_applicability_manager Coupon Rule (Manager) coupon.model_coupon_rule sales_team.group_sale_manager 1 1 1 0
6 access_coupon_salesman Coupon (Salesperson) coupon.model_coupon_coupon sales_team.group_sale_salesman 1 1 0 0
7 access_coupon_manager Coupon (Manager) coupon.model_coupon_coupon sales_team.group_sale_manager 1 1 1 0
8 access_reward_salesman Coupon Reward (Salesperson) coupon.model_coupon_reward sales_team.group_sale_salesman 1 0 0 0
9 access_reward_manager Coupon Reward (Manager) coupon.model_coupon_reward sales_team.group_sale_manager 1 1 1 0
10 access_sale_coupon_apply_code Apply Coupon model_sale_coupon_apply_code sales_team.group_sale_salesman 1 1 1 0
11 access_sale_coupon_generate Coupon Generation coupon.model_coupon_generate_wizard sales_team.group_sale_salesman 1 1 1 0
@@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="1">
<record id="sale_coupon_apply_code_rule" model="ir.rule">
<field name="name">Sale Coupon Apply Code Rule</field>
<field name="model_id" ref="model_sale_coupon_apply_code"/>
<field name="domain_force">[('create_uid', '=', user.id)]</field>
</record>
</odoo>
-9
View File
@@ -1,9 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import test_program_without_code_operations
from . import test_program_with_code_operations
from . import test_program_rules
from . import test_program_numbers
from . import test_program_multi_company
from . import test_sale_invoicing
-100
View File
@@ -1,100 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale.tests.test_sale_product_attribute_value_config import TestSaleProductAttributeValueCommon
class TestSaleCouponCommon(TestSaleProductAttributeValueCommon):
@classmethod
def setUpClass(cls):
super(TestSaleCouponCommon, cls).setUpClass()
# set currency to not rely on demo data and avoid possible race condition
cls.currency_ratio = 1.0
pricelist = cls.env.ref('product.list0')
pricelist.currency_id = cls._setup_currency(cls.currency_ratio)
# Set all the existing programs to active=False to avoid interference
cls.env['coupon.program'].search([]).write({'active': False})
# create partner for sale order.
cls.steve = cls.env['res.partner'].create({
'name': 'Steve Bucknor',
'email': 'steve.bucknor@example.com',
})
cls.empty_order = cls.env['sale.order'].create({
'partner_id': cls.steve.id
})
cls.uom_unit = cls.env.ref('uom.product_uom_unit')
# Taxes
cls.tax_15pc_excl = cls.env['account.tax'].create({
'name': "Tax 15%",
'amount_type': 'percent',
'amount': 15,
'type_tax_use': 'sale',
})
cls.tax_10pc_incl = cls.env['account.tax'].create({
'name': "10% Tax incl",
'amount_type': 'percent',
'amount': 10,
'price_include': True,
})
#products
cls.product_A = cls.env['product.product'].create({
'name': 'Product A',
'list_price': 100,
'sale_ok': True,
'taxes_id': [(6, 0, [cls.tax_15pc_excl.id])],
})
cls.product_B = cls.env['product.product'].create({
'name': 'Product B',
'list_price': 5,
'sale_ok': True,
'taxes_id': [(6, 0, [cls.tax_15pc_excl.id])],
})
cls.product_C = cls.env['product.product'].create({
'name': 'Product C',
'list_price': 100,
'sale_ok': True,
'taxes_id': [(6, 0, [])],
})
# Immediate Program By A + B: get B free
# No Conditions
cls.immediate_promotion_program = cls.env['coupon.program'].create({
'name': 'Buy A + 1 B, 1 B are free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
'reward_product_id': cls.product_B.id,
'rule_products_domain': "[('id', 'in', [%s])]" % (cls.product_A.id),
'active': True,
})
cls.code_promotion_program = cls.env['coupon.program'].create({
'name': 'Buy 1 A + Enter code, 1 A is free',
'promo_code_usage': 'code_needed',
'reward_type': 'product',
'reward_product_id': cls.product_A.id,
'rule_products_domain': "[('id', 'in', [%s])]" % (cls.product_A.id),
'active': True,
})
cls.code_promotion_program_with_discount = cls.env['coupon.program'].create({
'name': 'Buy 1 C + Enter code, 10 percent discount on C',
'promo_code_usage': 'code_needed',
'reward_type': 'discount',
'discount_type': 'percentage',
'discount_percentage': 10,
'rule_products_domain': "[('id', 'in', [%s])]" % (cls.product_C.id),
'active': True,
'discount_apply_on': 'on_order',
})
@@ -1,78 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestSaleCouponMultiCompany(TestSaleCouponCommon):
def setUp(self):
super(TestSaleCouponMultiCompany, self).setUp()
self.company_a = self.env.company
self.company_b = self.env['res.company'].create(dict(name="TEST"))
self.immediate_promotion_program_c2 = self.env['coupon.program'].create({
'name': 'Buy A + 1 B, 1 B are free',
'promo_code_usage': 'no_code_needed',
'reward_type': 'product',
'reward_product_id': self.product_B.id,
'rule_products_domain': "[('id', 'in', [%s])]" % (self.product_A.id),
'active': True,
'company_id': self.company_b.id,
})
def test_applicable_programs(self):
order = self.empty_order
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
def _get_applied_programs(order):
# temporary copy of sale_order._get_applied_programs
# to ensure each commit stays independent
# can be later removed and replaced in master.
return order.code_promo_program_id + order.no_code_promo_program_ids + order.applied_coupon_ids.mapped('program_id')
self.assertNotIn(self.immediate_promotion_program_c2, order._get_applicable_programs())
self.assertNotIn(self.immediate_promotion_program_c2, _get_applied_programs(order))
order_b = self.env["sale.order"].create({
'company_id': self.company_b.id,
'partner_id': order.partner_id.id,
})
order_b.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.assertNotIn(self.immediate_promotion_program, order_b._get_applicable_programs())
order_b.recompute_coupon_lines()
self.assertIn(self.immediate_promotion_program_c2, _get_applied_programs(order_b))
self.assertNotIn(self.immediate_promotion_program, _get_applied_programs(order_b))
File diff suppressed because it is too large Load Diff
@@ -1,394 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from datetime import datetime, timedelta
from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon
from odoo.exceptions import UserError
from odoo.fields import Date
class TestProgramRules(TestSaleCouponCommon):
# Test all the validity rules to allow a customer to have a reward.
# The check based on the products is already done in the basic operations test
def test_program_rules_partner_based(self):
# Test case: Based on the partners domain
self.immediate_promotion_program.write({'rule_partners_domain': "[('id', 'in', [%s])]" % (self.steve.id)})
order = self.empty_order
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 3, "The promo offert should have been applied as the partner is correct, the discount is not created")
order = self.env['sale.order'].create({'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied, the discount is created")
def test_program_rules_minimum_purchased_amount(self):
# Test case: Based on the minimum purchased
self.immediate_promotion_program.write({
'rule_minimum_amount': 1006,
'rule_minimum_amount_tax_inclusion': 'tax_excluded'
})
order = self.empty_order
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied as the purchased amount is not enough")
order = self.env['sale.order'].create({'partner_id': self.steve.id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '10 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
# 10*100 + 5 = 1005
self.assertEqual(len(order.order_line.ids), 2, "The promo offert should not be applied as the purchased amount is not enough")
self.immediate_promotion_program.rule_minimum_amount = 1005
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 3, "The promo offert should be applied as the purchased amount is now enough")
# 10*(100*1.15) + (5*1.15) = 10*115 + 5.75 = 1155.75
self.immediate_promotion_program.rule_minimum_amount = 1006
self.immediate_promotion_program.rule_minimum_amount_tax_inclusion = 'tax_included'
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 3, "The promo offert should be applied as the initial amount required is now tax included")
def test_program_rules_validity_dates_and_uses(self):
# Test case: Based on the validity dates and the number of allowed uses
self.immediate_promotion_program.write({
'rule_date_from': Date.to_string((datetime.now() - timedelta(days=7))),
'rule_date_to': Date.to_string((datetime.now() - timedelta(days=2))),
'maximum_use_number': 1,
})
order = self.empty_order
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied we're not between the validity dates")
self.immediate_promotion_program.write({
'rule_date_from': Date.to_string((datetime.now() - timedelta(days=7))),
'rule_date_to': Date.to_string((datetime.now() + timedelta(days=2))),
})
order = self.env['sale.order'].create({'partner_id': self.steve.id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 3, "The promo offert should have been applied as we're between the validity dates")
order = self.env['sale.order'].create({'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 10.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied as the number of uses is exceeded")
def test_program_rules_one_date(self):
# Test case: Based on the validity dates and the number of allowed uses
# VFE NOTE the .rule_id is necessary to ensure the dates constraints doesn't raise
# because the orm applies the related inverse one by one, raising the constraint...
self.immediate_promotion_program.rule_id.write({
'rule_date_from': False,
'rule_date_to': Date.to_string((datetime.now() - timedelta(days=2))),
})
order = self.empty_order
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertNotIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied we're not between the validity dates")
self.immediate_promotion_program.rule_id.write({
'rule_date_from': Date.to_string((datetime.now() + timedelta(days=1))),
'rule_date_to': False,
})
order.recompute_coupon_lines()
self.assertNotIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied we're not between the validity dates")
self.immediate_promotion_program.rule_id.write({
'rule_date_from': False,
'rule_date_to': Date.to_string((datetime.now() + timedelta(days=2))),
})
order.recompute_coupon_lines()
self.assertIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should have been applied as we're between the validity dates")
self.immediate_promotion_program.rule_id.write({
'rule_date_from': Date.to_string((datetime.now() - timedelta(days=1))),
'rule_date_to': False,
})
order.recompute_coupon_lines()
self.assertIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should have been applied as we're between the validity dates")
def test_program_rules_date(self):
# Test case: Based on the validity dates
# VFE NOTE the .rule_id is necessary to ensure the dates constraints doesn't raise
# because the orm applies the related inverse one by one, raising the constraint...
self.immediate_promotion_program.rule_id.write({
'rule_date_from': Date.to_string((datetime.now() - timedelta(days=7))),
'rule_date_to': Date.to_string((datetime.now() - timedelta(days=2))),
})
order = self.empty_order
order.write({
'date_order': Date.to_string((datetime.now() - timedelta(days=5))),
})
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
}),
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertNotIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied we're not between the validity dates")
self.immediate_promotion_program.rule_id.write({
'rule_date_from': Date.to_string((datetime.now() + timedelta(days=2))),
'rule_date_to': Date.to_string((datetime.now() + timedelta(days=7))),
})
order.recompute_coupon_lines()
self.assertNotIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 2, "The promo offert shouldn't have been applied we're not between the validity dates")
self.immediate_promotion_program.rule_id.write({
'rule_date_from': Date.to_string((datetime.now() - timedelta(days=2))),
'rule_date_to': Date.to_string((datetime.now() + timedelta(days=2))),
})
order.recompute_coupon_lines()
self.assertIn(self.immediate_promotion_program, order._get_applicable_programs())
self.assertEqual(len(order.order_line.ids), 3, "The promo offer should have been applied as we're between the validity dates")
def test_program_rules_coupon_qty_and_amount_remove_not_eligible(self):
''' This test will:
* Check quantity and amount requirements works as expected (since it's slightly different from a promotion_program)
* Ensure that if a reward from a coupon_program was allowed and the conditions are not met anymore,
the reward will be removed on recompute.
'''
self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
order = self.empty_order
program = self.env['coupon.program'].create({
'name': 'Get 10% discount if buy at least 4 Product A and $320',
'program_type': 'coupon_program',
'reward_type': 'discount',
'discount_type': 'percentage',
'discount_percentage': 10.0,
'rule_products_domain': "[('id', 'in', [%s])]" % (self.product_A.id),
'rule_min_quantity': 3,
'rule_minimum_amount': 320.00,
})
sol1 = self.env['sale.order.line'].create({
'product_id': self.product_A.id,
'name': 'Product A',
'product_uom_qty': 2.0,
'order_id': order.id,
})
sol2 = self.env['sale.order.line'].create({
'product_id': self.product_B.id,
'name': 'Product B',
'product_uom_qty': 4.0,
'order_id': order.id,
})
# Default value for coupon generate wizard is generate by quantity and generate only one coupon
self.env['coupon.generate.wizard'].with_context(active_id=program.id).create({}).generate_coupon()
coupon = program.coupon_ids[0]
# Not enough amount since we only have 220 (100*2 + 5*4)
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': coupon.code
}).process_coupon()
sol2.product_uom_qty = 24
# Not enough qty since we only have 3 Product A (Amount is ok: 100*2 + 5*24 = 320)
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': coupon.code
}).process_coupon()
sol1.product_uom_qty = 3
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': coupon.code
}).process_coupon()
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 3, "The order should contains the Product A line, the Product B line and the discount line")
self.assertEqual(coupon.state, 'used', "The coupon should be set to Consumed as it has been used")
sol1.product_uom_qty = 2
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The discount line should have been removed as we don't meet the program requirements")
self.assertEqual(coupon.state, 'new', "The coupon should be reset to Valid as it's reward got removed")
def test_program_rules_promotion_use_best(self):
''' This test will:
* Verify the best global promotion according to the
current sale order is used.
'''
self.immediate_promotion_program.active = False # Avoid having this program to add rewards on this test
order = self.empty_order
program_5pc = self.env['coupon.program'].create({
'name': 'Get 5% discount if buy at least 2 Product',
'program_type': 'promotion_program',
'reward_type': 'discount',
'discount_type': 'percentage',
'discount_percentage': 5.0,
'rule_min_quantity': 2,
'promo_code_usage': 'no_code_needed',
})
program_10pc = self.env['coupon.program'].create({
'name': 'Get 10% discount if buy at least 4 Product',
'program_type': 'promotion_program',
'reward_type': 'discount',
'discount_type': 'percentage',
'discount_percentage': 10.0,
'rule_min_quantity': 4,
'promo_code_usage': 'no_code_needed',
})
sol = self.env['sale.order.line'].create({
'product_id': self.product_A.id,
'name': 'Product A',
'product_uom_qty': 1.0,
'order_id': order.id,
})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 1, "The order should only contains the Product A line")
sol.product_uom_qty = 3
order.recompute_coupon_lines()
discounts = set(order.order_line.mapped('name')) - {'Product A'}
self.assertEqual(len(discounts), 1, "The order should contains the Product A line and a discount")
# The name of the discount is dynamically changed to smth looking like:
# "Discount: Get 5% discount if buy at least 2 Product - On product with following tax: Tax 15.00%"
self.assertTrue('Get 5% discount' in discounts.pop(), "The discount should be a 5% discount")
sol.product_uom_qty = 5
order.recompute_coupon_lines()
discounts = set(order.order_line.mapped('name')) - {'Product A'}
self.assertEqual(len(discounts), 1, "The order should contains the Product A line and a discount")
self.assertTrue('Get 10% discount' in discounts.pop(), "The discount should be a 10% discount")
@@ -1,344 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon
from odoo.exceptions import UserError
class TestProgramWithCodeOperations(TestSaleCouponCommon):
# Test the basic operation (apply_coupon) on an coupon program on which we should
# apply the reward when the code is correct or remove the reward automatically when the reward is
# not valid anymore.
def test_program_usability(self):
# After clicking "Generate coupons", there is no domain so it shows "Match all records".
# But when you click, domain is false (default field value; empty string) so it won't generate anything.
# This is even more weird because if you add something in the domain and then delete it,
# you visually come back to the initial state except the domain became '[]' instead of ''.
# In this case, it will generate the coupon for every partner.
# Thus, we should ensure that if you leave the domain untouched, it generates a coupon for each partner
# as hinted on the screen ('Match all records (X records)')
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'generation_type': 'nbr_customer',
}).generate_coupon()
self.assertEqual(len(self.code_promotion_program.coupon_ids), len(self.env['res.partner'].search([])), "It should have generated a coupon for every partner")
def test_program_basic_operation_coupon_code(self):
# Test case: Generate a coupon for my customer, and add a reward then remove it automatically
self.code_promotion_program.reward_type = 'discount'
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'generation_type': 'nbr_customer',
'partners_domain': "[('id', 'in', [%s])]" % (self.steve.id),
}).generate_coupon()
coupon = self.code_promotion_program.coupon_ids
# Test the valid code on a wrong sales order
wrong_partner_order = self.env['sale.order'].create({
'partner_id': self.env['res.partner'].create({'name': 'My Partner'}).id,
})
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=wrong_partner_order.id).create({
'coupon_code': coupon.code
}).process_coupon()
# Test now on a valid sales order
order = self.empty_order
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': coupon.code
}).process_coupon()
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2)
self.assertEqual(coupon.state, 'used')
# Remove the product A from the sale order
order.write({'order_line': [(2, order.order_line[0].id, False)]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 0)
self.assertEqual(coupon.state, 'new')
def test_program_coupon_double_consuming(self):
# Test case:
# - Generate a coupon
# - add to a sale order A, cancel the sale order
# - add to a sale order B, confirm the order
# - go back to A, reset to draft and confirm
self.code_promotion_program.reward_type = 'discount'
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program.id).create({
'generation_type': 'nbr_coupon',
'nbr_coupons': 1,
}).generate_coupon()
coupon = self.code_promotion_program.coupon_ids
sale_order_a = self.empty_order.copy()
sale_order_b = self.empty_order.copy()
sale_order_a.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['sale.coupon.apply.code'].with_context(active_id=sale_order_a.id).create({
'coupon_code': coupon.code
}).process_coupon()
sale_order_a.recompute_coupon_lines()
self.assertEqual(len(sale_order_a.order_line.ids), 2)
self.assertEqual(coupon.state, 'used')
self.assertEqual(coupon.sales_order_id, sale_order_a)
sale_order_a.action_cancel()
sale_order_b.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['sale.coupon.apply.code'].with_context(active_id=sale_order_b.id).create({
'coupon_code': coupon.code
}).process_coupon()
sale_order_b.recompute_coupon_lines()
self.assertEqual(len(sale_order_b.order_line.ids), 2)
self.assertEqual(coupon.state, 'used')
self.assertEqual(coupon.sales_order_id, sale_order_b)
sale_order_b.action_confirm()
sale_order_a.action_draft()
sale_order_a.action_confirm()
# reward line removed automatically
self.assertEqual(len(sale_order_a.order_line.ids), 1)
def test_coupon_code_with_pricelist(self):
# Test case: Generate a coupon (10% discount) and apply it on an order with a specific pricelist (10% discount)
self.env['coupon.generate.wizard'].with_context(active_id=self.code_promotion_program_with_discount.id).create({
'generation_type': 'nbr_coupon',
'nbr_coupons': 1,
}).generate_coupon()
coupon = self.code_promotion_program_with_discount.coupon_ids
first_pricelist = self.env['product.pricelist'].create({
'name': 'First pricelist',
'discount_policy': 'with_discount',
'item_ids': [(0, 0, {
'compute_price': 'percentage',
'base': 'list_price',
'percent_price': 10,
'applied_on': '3_global',
'name': 'First discount'
})]
})
order = self.empty_order
order.pricelist_id = first_pricelist
order.write({'order_line': [
(0, False, {
'product_id': self.product_C.id,
'name': '1 Product C',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': coupon.code
}).process_coupon()
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2)
self.assertEqual(coupon.state, 'used')
self.assertEqual(order.amount_total, 81, "SO total should be 81: (10% of 100 with pricelist) + 10% of 90 with coupon code")
def test_on_next_order_reward_promotion_program(self):
# The flow:
# 1. Create a program `A` that gives a free `Product B` on next order if you buy a an `product A`
# This program should be code_needed with code `free_B_on_next_order`
# 2. Create a program `B` that gives 10% discount on next order automatically
# 3. Create a SO with a `third product` and recompute coupon, you SHOULD get a coupon (from program `B`) for your next order that will discount 10%
# 4. Try to apply `A`, it should error since we did not buy any product A.
# 5. Add a product A to the cart and try to apply `A` again, this time it should work
# 6. Verify you have 2 generated coupons and validate the SO (so the 2 generated coupons will be valid)
# 7. Create a new SO (with the same partner) and try to apply coupon generated by `A`. it SHOULD error since we don't have any `Product B` in the cart
# 8. Add a Product B in the cart
# 9. Try to apply once again coupon generated by `A`, it should give you the free product B
# 10. Try to apply coupon generated by `B`, it should give you 10% discount.
# => SO will then be 0$ until we recompute the order lines
# 1.
self.immediate_promotion_program.write({
'promo_applicability': 'on_next_order',
'promo_code_usage': 'code_needed',
'promo_code': 'free_B_on_next_order',
})
# 2.
self.p1 = self.env['coupon.program'].create({
'name': 'Code for 10% on next order',
'discount_type': 'percentage',
'discount_percentage': 10.0,
'program_type': 'promotion_program',
'promo_code_usage': 'no_code_needed',
'promo_applicability': 'on_next_order',
})
# 3.
order = self.empty_order.copy()
self.third_product = self.env['product.product'].create({
'name': 'Thrid Product',
'list_price': 5,
'sale_ok': True
})
order.write({'order_line': [
(0, False, {
'product_id': self.third_product.id,
'name': '1 Third Product',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(self.p1.coupon_ids.ids), 1, "You should get a coupon for you next order that will offer 10% discount")
# 4.
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': 'free_B_on_next_order'
}).process_coupon()
# 5.
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': 'free_B_on_next_order'
}).process_coupon()
# 6.
self.assertEqual(len(order.generated_coupon_ids), 2, "You should get a second coupon for your next order that will offer a free Product B")
order.action_confirm()
# 7.
order_bis = self.empty_order
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order_bis.id).create({
'coupon_code': order.generated_coupon_ids[1].code
}).process_coupon()
# 8.
order_bis.write({'order_line': [
(0, False, {
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
# 9.
self.env['sale.coupon.apply.code'].with_context(active_id=order_bis.id).create({
'coupon_code': order.generated_coupon_ids[1].code
}).process_coupon()
self.assertEqual(len(order_bis.order_line), 2, "You should get a free Product B")
# 10.
self.env['sale.coupon.apply.code'].with_context(active_id=order_bis.id).create({
'coupon_code': order.generated_coupon_ids[0].code
}).process_coupon()
self.assertEqual(len(order_bis.order_line), 3, "You should get a 10% discount line")
self.assertEqual(order_bis.amount_total, 0, "SO total should be null: (Paid product - Free product = 0) + 10% of nothing")
def test_on_next_order_reward_promotion_program_with_requirements(self):
self.immediate_promotion_program.write({
'promo_applicability': 'on_next_order',
'promo_code_usage': 'code_needed',
'promo_code': 'free_B_on_next_order',
'rule_minimum_amount': 700,
'rule_minimum_amount_tax_inclusion': 'tax_excluded'
})
order = self.empty_order.copy()
self.product_A.lst_price = 700
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': 'free_B_on_next_order'
}).process_coupon()
self.assertEqual(len(self.immediate_promotion_program.coupon_ids.ids), 1, "You should get a coupon for you next order that will offer a free product B")
order_bis = self.empty_order
order_bis.write({'order_line': [
(0, False, {
'product_id': self.product_B.id,
'name': '1 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
with self.assertRaises(UserError):
# It should error since we did not validated the previous SO, so the coupon is `reserved` but not `new`
self.env['sale.coupon.apply.code'].with_context(active_id=order_bis.id).create({
'coupon_code': order.generated_coupon_ids[0].code
}).process_coupon()
order.action_confirm()
# It should not error even if the SO does not have the requirements (700$ and 1 product A), since these requirements where only used to generate the coupon that we are now applying
self.env['sale.coupon.apply.code'].with_context(active_id=order_bis.id).create({
'coupon_code': order.generated_coupon_ids[0].code
}).process_coupon()
self.assertEqual(len(order_bis.order_line), 2, "You should get 1 regular product_B and 1 free product_B")
order_bis.recompute_coupon_lines()
self.assertEqual(len(order_bis.order_line), 2, "Free product from a coupon generated from a promotion program on next order should not dissapear")
def test_edit_and_reapply_promotion_program(self):
# The flow:
# 1. Create a program auto applied, giving a fixed amount discount
# 2. Create a SO and apply the program
# 3. Change the program, requiring a mandatory code
# 4. Reapply the program on the same SO via code
# 1.
self.p1 = self.env['coupon.program'].create({
'name': 'Promo fixed amount',
'promo_code_usage': 'no_code_needed',
'discount_type': 'fixed_amount',
'discount_fixed_amount': 10.0,
'program_type': 'promotion_program',
})
# 2.
order = self.empty_order.copy()
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line), 2, "You should get a discount line")
# 3.
self.p1.write({
'promo_code_usage': 'code_needed',
'promo_code': 'test',
})
order.recompute_coupon_lines()
# 4.
with self.assertRaises(UserError):
self.env['sale.coupon.apply.code'].with_context(active_id=order.id).create({
'coupon_code': 'test'
}).process_coupon()
self.assertEqual(len(order.order_line), 2, "You should get a discount line")
@@ -1,59 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon
class TestProgramWithoutCodeOperations(TestSaleCouponCommon):
# Test some basic operation (create, write, unlink) on an immediate coupon program on which we should
# apply or remove the reward automatically, as there's no program code.
def test_immediate_program_basic_operation(self):
# 2 products A are needed
self.immediate_promotion_program.write({'rule_min_quantity': 2.0})
order = self.empty_order
# Test case 1 (1 A): Assert that no reward is given, as the product B is missing
order.write({'order_line': [
(0, False, {
'product_id': self.product_A.id,
'name': '1 Product A',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 1, "The promo offer shouldn't have been applied as the product B isn't in the order")
# Test case 2 (1 A 1 B): Assert that no reward is given, as the product B is not present in the correct quantity
order.write({'order_line': [
(0, False, {
'product_id': self.product_B.id,
'name': '2 Product B',
'product_uom': self.uom_unit.id,
'product_uom_qty': 1.0,
})
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo offer shouldn't have been applied as 2 product A aren't in the order")
# Test case 3 (2 A 1 B): Assert that the reward is given as the product B is now in the order
order.write({'order_line': [(1, order.order_line[0].id, {'product_uom_qty': 2.0})]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 3, "The promo offert should have been applied, the discount is not created")
# Test case 4 (1 A 1 B): Assert that the reward is removed as we don't buy 2 products B anymore
order.write({'order_line': [(1, order.order_line[0].id, {'product_uom_qty': 1.0})]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 2, "The promo reward should have been removed as the rules are not matched anymore")
self.assertEqual(order.order_line[0].product_id.id, self.product_A.id, "The wrong line has been removed")
self.assertEqual(order.order_line[1].product_id.id, self.product_B.id, "The wrong line has been removed")
# Test case 5 (1 B): Assert that the reward is removed when the order is modified and doesn't match the rules anymore
order.write({'order_line': [
(1, order.order_line[0].id, {'product_uom_qty': 2.0}),
(2, order.order_line[0].id, False)
]})
order.recompute_coupon_lines()
self.assertEqual(len(order.order_line.ids), 1, "The promo reward should have been removed as the rules are not matched anymore")
self.assertEqual(order.order_line.product_id.id, self.product_B.id, "The wrong line has been removed")
@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo.addons.sale_coupon.tests.common import TestSaleCouponCommon
from odoo.exceptions import UserError
from odoo.tests import tagged
@tagged('post_install', '-at_install')
class TestSaleInvoicing(TestSaleCouponCommon):
def test_invoicing_order_with_promotions(self):
discount_coupon_program = self.env['coupon.program'].create({
'name': '10% Discount', # Default behavior
'program_type': 'coupon_program',
'reward_type': 'discount',
'discount_apply_on': 'on_order',
'promo_code_usage': 'no_code_needed',
})
# Override the default invoice_policy on products
discount_coupon_program.discount_line_product_id.invoice_policy = 'order'
product = self.env['product.product'].create({
'invoice_policy': 'delivery',
'name': 'Product invoiced on delivery',
'lst_price': 500,
})
order = self.empty_order
order.write({
'order_line': [
(0, 0, {
'product_id': product.id,
})
]
})
order.recompute_coupon_lines()
# Order is not confirmed, there shouldn't be any invoiceable line
invoiceable_lines = order._get_invoiceable_lines()
self.assertEqual(len(invoiceable_lines), 0)
order.action_confirm()
invoiceable_lines = order._get_invoiceable_lines()
# Product was not delivered, we cannot invoice
# the product line nor the promotion line
self.assertEqual(len(invoiceable_lines), 0)
with self.assertRaises(UserError):
order._create_invoices()
order.order_line[0].qty_delivered = 1
# Product is delivered, the two lines can be invoiced.
invoiceable_lines = order._get_invoiceable_lines()
self.assertEqual(order.order_line, invoiceable_lines)
account_move = order._create_invoices()
self.assertEqual(len(account_move.invoice_line_ids), 2)
@@ -1,68 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Menu Items -->
<menuitem
id="menu_coupon_type_config"
action="coupon.coupon_program_action_coupon_program"
parent="sale.product_menu_catalog"
name="Coupon Programs"
groups="sales_team.group_sale_manager"
sequence="5"
/>
<menuitem
id="menu_promotion_type_config"
action="coupon.coupon_program_action_promo_program"
parent="sale.product_menu_catalog"
name="Promotion Programs"
groups="sales_team.group_sale_manager"
sequence="4"
/>
<!-- Form Views -->
<record id="sale_coupon_program_view_coupon_program_form" model="ir.ui.view">
<field name="name">coupon.program.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.coupon_program_view_coupon_program_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='coupon_count']/.." position="before">
<button class="oe_stat_button" type="object" icon="fa-usd" name="action_view_sales_orders">
<field name="order_count" string="Sales" widget="statinfo"/>
</button>
</xpath>
</field>
</record>
<record id="sale_coupon_program_view_promo_program_form" model="ir.ui.view">
<field name="name">coupon.program.form</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.coupon_program_view_promo_program_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='coupon_count']/.." position="before">
<button class="oe_stat_button" type="object" icon="fa-usd" name="action_view_sales_orders">
<field name="order_count" string="Sales" widget="statinfo"/>
</button>
</xpath>
</field>
</record>
<!-- Kanban Views -->
<record id="sale_coupon_view_coupon_program_kanban" model="ir.ui.view">
<field name="name">coupon.program.kanban</field>
<field name="model">coupon.program</field>
<field name="inherit_id" ref="coupon.view_coupon_program_kanban" />
<field name="arch" type="xml">
<xpath expr="//div[hasclass('coupon-count-label')]" position="after">
<div class="col-4 text-center">
<strong>Sales</strong>
</div>
</xpath>
<xpath expr="//div[hasclass('coupon-count-value')]" position="after">
<div class="col-4 text-center">
<field name="order_count"/>
</div>
</xpath>
</field>
</record>
</odoo>
-28
View File
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_coupon_view_tree" model="ir.ui.view">
<field name="name">coupon.coupon.tree</field>
<field name="model">coupon.coupon</field>
<field name="inherit_id" ref="coupon.coupon_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="order_id"/>
</xpath>
</field>
</record>
<record id="sale_coupon_view_form" model="ir.ui.view">
<field name="name">coupon.coupon.form</field>
<field name="model">coupon.coupon</field>
<field name="inherit_id" ref="coupon.coupon_view_form"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='partner_id']" position="after">
<field name="order_id" invisible="1"/>
<field name="sales_order_id" invisible="1"/>
<field name="order_id" readonly="1" attrs="{'invisible': [('order_id', '=', False)]}" />
<field name="sales_order_id" readonly="1" attrs="{'invisible': [('sales_order_id', '=', False)]}" />
</xpath>
</field>
</record>
</odoo>
@@ -1,22 +0,0 @@
<?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.coupon</field>
<field name="model">res.config.settings</field>
<field name="inherit_id" ref="sale.res_config_settings_view_form"/>
<field name="arch" type="xml">
<xpath expr="//div[@id='sale_coupon']" position="after">
<div class="content-group">
<div class="mt8" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(coupon.coupon_program_action_promo_program)d" icon="fa-arrow-right" type="action" string="Promotion Programs" class="btn-link"/>
</div>
<div class="mt8" attrs="{'invisible': [('module_sale_coupon', '=', False)]}">
<button name="%(coupon.coupon_program_action_coupon_program)d" icon="fa-arrow-right" type="action" string="Coupon Programs" class="btn-link"/>
</div>
</div>
</xpath>
</field>
</record>
</odoo>
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_order_view_form" model="ir.ui.view">
<field name="name">sale.order.form.inherit.coupon</field>
<field name="model">sale.order</field>
<field name="inherit_id" ref="sale.view_order_form" />
<field name="priority">10</field>
<field name="arch" type="xml">
<xpath expr="//group[@name='note_group']" position="before">
<div class="oe_right">
<button name="%(sale_coupon.sale_coupon_apply_code_action)d" class="btn btn-secondary"
string="Coupon" type="action" groups="base.group_user" states="draft,sent,sale"/>
<button name="recompute_coupon_lines" class="btn btn-secondary" string="Promotions"
help="When clicked, the content of the order will be checked to detect (and apply) possible promotion programs."
type="object" states="draft,sent,sale"/>
</div>
</xpath>
</field>
</record>
<record id="sale_order_action" model="ir.actions.act_window">
<field name="name">Sales Order</field>
<field name="res_model">sale.order</field>
<field name="view_id" ref="sale.view_order_tree"/>
<field name="domain">[('state', '!=', 'cancel')]</field>
<field name="context">{}</field>
</record>
</odoo>
-4
View File
@@ -1,4 +0,0 @@
# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from . import sale_coupon_apply_code
@@ -1,54 +0,0 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
class SaleCouponApplyCode(models.TransientModel):
_name = 'sale.coupon.apply.code'
_rec_name = 'coupon_code'
_description = 'Sales Coupon Apply Code'
coupon_code = fields.Char(string="Code", required=True)
def process_coupon(self):
"""
Apply the entered coupon code if valid, raise an UserError otherwise.
"""
sales_order = self.env['sale.order'].browse(self.env.context.get('active_id'))
error_status = self.apply_coupon(sales_order, self.coupon_code)
if error_status.get('error', False):
raise UserError(error_status.get('error', False))
if error_status.get('not_found', False):
raise UserError(error_status.get('not_found', False))
def apply_coupon(self, order, coupon_code):
error_status = {}
program = self.env['coupon.program'].search([('promo_code', '=', coupon_code)])
if program:
error_status = program._check_promo_code(order, coupon_code)
if not error_status:
if program.promo_applicability == 'on_next_order':
# Avoid creating the coupon if it already exist
if program.discount_line_product_id.id not in order.generated_coupon_ids.filtered(lambda coupon: coupon.state in ['new', 'reserved']).mapped('discount_line_product_id').ids:
coupon = order._create_reward_coupon(program)
return {
'generated_coupon': {
'reward': coupon.program_id.discount_line_product_id.name,
'code': coupon.code,
}
}
else: # The program is applied on this order
order._create_reward_line(program)
order.code_promo_program_id = program
else:
coupon = self.env['coupon.coupon'].search([('code', '=', coupon_code)], limit=1)
if coupon:
error_status = coupon._check_coupon_code(order.date_order.date(), order.partner_id.id, order=order)
if not error_status:
order._create_reward_line(coupon.program_id)
order.applied_coupon_ids += coupon
coupon.write({'state': 'used'})
else:
error_status = {'not_found': _('This coupon is invalid (%s).') % (coupon_code)}
return error_status
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="sale_coupon_apply_code_view_form" model="ir.ui.view">
<field name="name">sale.coupon.apply.code.form</field>
<field name="model">sale.coupon.apply.code</field>
<field name="arch" type="xml">
<form string="Apply coupon">
<group>
<group>
<field name="coupon_code"/>
</group>
</group>
<footer>
<button name="process_coupon" string="Apply" type="object" class="oe_highlight" data-hotkey="q"/>
<button special="cancel" data-hotkey="z" string="Cancel"/>
</footer>
</form>
</field>
</record>
<record id="sale_coupon_apply_code_action" model="ir.actions.act_window">
<field name="name">Enter Promotion or Coupon Code</field>
<field name="res_model">sale.coupon.apply.code</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="view_id" ref="sale_coupon_apply_code_view_form"/>
</record>
</odoo>