diff --git a/membership_initial_discount/README.rst b/membership_initial_discount/README.rst new file mode 100644 index 0000000..037ee88 --- /dev/null +++ b/membership_initial_discount/README.rst @@ -0,0 +1,101 @@ +=========================== +Initial fee for memberships +=========================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png + :target: https://odoo-community.org/page/development-status + :alt: Beta +.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fvertical--association-lightgray.png?logo=github + :target: https://github.com/OCA/vertical-association/tree/14.0/membership_initial_fee + :alt: OCA/vertical-association +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/vertical-association-14-0/vertical-association-14-0-membership_initial_fee + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/208/14.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +Charge an initial fee when a partner is invoiced for the first time with a +member product. + +**Table of contents** + +.. contents:: + :local: + +Usage +===== + +Define a member product, and select 'Fixed amount' or 'Percentage of the price' +for invoicing an extra charge in the first invoice that is created with this +member product. + +By default, a line with the description *Membership initial fee*. If you want +to change this text, you can set a different sale description in the product +used for the fee. + +Known issues / Roadmap +====================== + +* Add initial fee information to membership analysis. +* Add some criteria for adding initial fee: + * Add initial fee if last membership ended x weeks/months/years ago + * Add initial fee if last membership product_id is different + +Bug Tracker +=========== + +Bugs are tracked on `GitHub Issues `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us smashing it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Tecnativa + +Contributors +~~~~~~~~~~~~ + +* `Tecnativa `__: + + * Pedro M. Baeza + * Rafael Blasco + * David Vidal + +* `Onestein `__: + + * Andrea Stirpe + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +OCA, or the Odoo Community Association, is a nonprofit organization whose +mission is to support the collaborative development of Odoo features and +promote its widespread use. + +This module is part of the `OCA/vertical-association `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/membership_initial_discount/__init__.py b/membership_initial_discount/__init__.py new file mode 100644 index 0000000..95ac574 --- /dev/null +++ b/membership_initial_discount/__init__.py @@ -0,0 +1,2 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 +from . import models diff --git a/membership_initial_discount/__manifest__.py b/membership_initial_discount/__manifest__.py new file mode 100644 index 0000000..3a032af --- /dev/null +++ b/membership_initial_discount/__manifest__.py @@ -0,0 +1,17 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 + +{ + "name": "Initial discount on 1st membership", + "version": "1.0.0", + "license": "AGPL-3", + "category": "Association", + "author": "RemiFr82", + "website": "https://remifr82.me", + # "website": "https://github.com/OCA/vertical-association", + "depends": [ + "membership", + ], + "data": [ + "views/product_template_views.xml", + ], +} diff --git a/membership_initial_discount/i18n/fr.po b/membership_initial_discount/i18n/fr.po new file mode 100644 index 0000000..714f305 --- /dev/null +++ b/membership_initial_discount/i18n/fr.po @@ -0,0 +1,65 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * membership_initial_discount +# +# Translators: +# OCA Transbot , 2017 +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-06-08 02:41+0000\n" +"PO-Revision-Date: 2022-05-30 18:05+0000\n" +"Last-Translator: Abdourahmane Wone \n" +"Language-Team: French (https://www.transifex.com/oca/teams/23907/fr/)\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.3.2\n" + +#. module: membership_initial_discount +#: model:ir.model.fields,field_description:membership_initial_discount.field_product_product__fixed_discount +#: model:ir.model.fields,field_description:membership_initial_discount.field_product_template__fixed_discount +msgid "Discount amount" +msgstr "Montant de la remise" + +#. module: membership_initial_discount +#: selection:product.template,initial_discount:0 +msgid "Fixed amount" +msgstr "Montant fixe" + +#. module: membership_initial_discount +#: model:ir.model.fields,field_description:membership_initial_discount.field_product_product__initial_discount +#: model:ir.model.fields,field_description:membership_initial_discount.field_product_template__initial_discount +#: model_terms:ir.ui.view,arch_db:membership_initial_discount.membership_products_form_initial_discount +msgid "Initial discount" +msgstr "Remise initiale" + +#. module: membership_initial_discount +#: model:ir.model,name:membership_initial_discount.model_account_invoice_line +msgid "Invoice Line" +msgstr "Ligne de facture" + +#. module: membership_initial_discount +#: selection:product.template,initial_discount:0 +msgid "No initial discount" +msgstr "Pas de remise initiale" + +#. module: membership_initial_discount +#: model:ir.model.fields,field_description:membership_initial_discount.field_product_product__percentage_discount +#: model:ir.model.fields,field_description:membership_initial_discount.field_product_template__percentage_discount +msgid "Discount (%)" +msgstr "Remise en %" + +#. module: membership_initial_discount +#: selection:product.template,initial_discount:0 +msgid "Percentage of the price" +msgstr "Pourcentage du prix" + +#. module: membership_initial_discount +#: model:ir.model,name:membership_initial_discount.model_product_template +msgid "Product Template" +msgstr "Modèle d'article" + diff --git a/membership_initial_discount/models/__init__.py b/membership_initial_discount/models/__init__.py new file mode 100644 index 0000000..90d56e0 --- /dev/null +++ b/membership_initial_discount/models/__init__.py @@ -0,0 +1,6 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 +from . import account_move_line +from . import membership +from . import product_product +from . import product_template +from . import res_partner diff --git a/membership_initial_discount/models/account_move_line.py b/membership_initial_discount/models/account_move_line.py new file mode 100644 index 0000000..175695c --- /dev/null +++ b/membership_initial_discount/models/account_move_line.py @@ -0,0 +1,65 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 + +from odoo import api, models, _ + + +class AccountMoveLine(models.Model): + _inherit = "account.move.line" + + def _get_computed_price_unit(self): + res = super()._get_computed_price_unit() + if self.move_id.is_invoice() and self.initial_membership_check(): + product = self.product_id + if product.initial_discount == "fixed" and product.fixed_discount: + res -= product.fixed_discount + return res + + @api.onchange("product_id") + def _onchange_product_id(self): + super()._onchange_product_id() + for line in self: + if not line.product_id or line.display_type in ( + "line_section", + "line_note", + ): + continue + if line.move_id.is_invoice() and line.initial_membership_check(): + product = line.product_id + if product.initial_discount == "percent" and product.percent_discount: + line.discount = product.percent_discount + else: + pass + + def initial_membership_check(self): + """ + Inherit this method to implement a custom method + to decide whether or not to create the initial discount + + :return: + """ + self.ensure_one() + product = self.product_id + if not product or not product.membership or product.initial_discount == "none": + return False + # If we are associated to another partner membership, evaluate that + # partner lines + partner = self.partner_id.associate_member or self.move_id.partner_id + # By default, partner to check is the partner of the invoice, but + # if a special method is found, overwritten in other modules, then + # the partner is got from that method + if hasattr(self, "_get_partner_for_membership"): # pragma: no cover + partner = self._get_partner_for_membership() + # See if partner has any membership line to decide whether or not + # to create the initial discount + member_lines = self.env["membership.membership_line"].search( + [ + ("partner", "=", partner.id), + ( + "account_invoice_line", + "not in", + [self.id or self._origin.id], + ), + ("state", "not in", ["none", "canceled"]), + ] + ) + return not bool(member_lines) diff --git a/membership_initial_discount/models/membership.py b/membership_initial_discount/models/membership.py new file mode 100644 index 0000000..6f12a0c --- /dev/null +++ b/membership_initial_discount/models/membership.py @@ -0,0 +1,18 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import api, models + + +class MembershipLine(models.Model): + _inherit = "membership.membership_line" + + @api.model_create_multi + def create(self, vals_list): + res = super().create(vals_list) + for line in res: + discount = line.account_invoice_line.discount + if discount: + line.member_price *= discount / 100.0 + + return res diff --git a/membership_initial_discount/models/product_product.py b/membership_initial_discount/models/product_product.py new file mode 100644 index 0000000..3ddb988 --- /dev/null +++ b/membership_initial_discount/models/product_product.py @@ -0,0 +1,26 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +DISCOUNTS = [ + ("none", _("No initial discount")), + ("fixed", _("Fixed amount")), + ("percent", _("Percentage of the price")), +] + + +class ProductProduct(models.Model): + _inherit = "product.product" + + @api.constrains("lst_price", "initial_discount", "fixed_discount") + def check_discount_fixed(self): + for product in self: + if ( + product.initial_discount == "fixed" + and product.lst_price <= product.initial_discount + ): + raise ValidationError( + _( + "Fixed discount for 1st membership must be less than the product template or any of its variants sale price." + ) + ) diff --git a/membership_initial_discount/models/product_template.py b/membership_initial_discount/models/product_template.py new file mode 100644 index 0000000..8cae669 --- /dev/null +++ b/membership_initial_discount/models/product_template.py @@ -0,0 +1,58 @@ +# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0 +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + +DISCOUNTS = [ + ("none", _("No initial discount")), + ("fixed", _("Fixed amount")), + ("percent", _("Percentage of the price")), +] + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + initial_discount = fields.Selection( + selection=DISCOUNTS, + default="none", + string="Initial discount", + required=True, + ) + fixed_discount = fields.Float( + string="Discount amount", + digits="Product Price", + ) + percent_discount = fields.Float( + string="Discount (%)", + digits=(12, 2), + ) + + @api.constrains("list_price", "initial_discount", "fixed_discount") + def check_fixed_discount(self): + for product in self: + if ( + product.initial_discount == "fixed" + and product.list_price <= product.fixed_discount + ): + raise ValidationError( + _( + "Fixed discount for 1st membership must be less than the product template, or any of its variants, sale price." + ) + ) + + @api.constrains("initial_discount", "percent_discount") + def check_percent_discount(self): + for product in self: + if product.initial_discount == "percent" and product.percent_discount < 0.0: + raise ValidationError( + _( + 'Percent discount cannot handle a negative value.\nIf you wan to apply extra fees, please install "Membership initial fee" module (OCA/vertical-association)' + ) + ) + elif ( + product.initial_discount == "percent" + and product.percent_discount >= 100.0 + ): + raise ValidationError( + _("Percent discount must handle a value smaller than 100%.") + ) diff --git a/membership_initial_discount/models/res_partner.py b/membership_initial_discount/models/res_partner.py new file mode 100644 index 0000000..a64914b --- /dev/null +++ b/membership_initial_discount/models/res_partner.py @@ -0,0 +1,65 @@ +from odoo import models, api, _ +from odoo.exceptions import UserError + + +class ResPartner(models.Model): + _inherit = "res.partner" + + def initial_membership_check(self): + self.ensure_one() + partner = self.associate_member or self + member_lines = self.env["membership.membership_line"].search( + [ + ("partner", "=", partner.id), + ("state", "not in", ["none", "canceled"]), + ] + ) + return not bool(member_lines) + + def prepare_membership_invoice_vals(self, product, amount): + discount = 0.0 + if self.initial_membership_check(): + if product.initial_discount == "fixed" and product.fixed_discount: + print("fixed discount") + fixed_disc = product.fixed_discount + amount -= fixed_disc + elif product.initial_discount == "percent" and product.percent_discount: + print("percent discount") + discount = product.percent_discount + else: + pass + vals = { + "move_type": "out_invoice", + "partner_id": self.id, + "invoice_line_ids": [ + ( + 0, + None, + { + "product_id": product.id, + "quantity": 1, + "price_unit": amount, + "discount": discount, + "tax_ids": [(6, 0, product.taxes_id.ids)], + }, + ) + ], + } + return vals + + def create_membership_invoice(self, product, amount): + """Create Customer Invoice of Membership for partners.""" + invoice_vals_list = [] + for partner in self: + addr = partner.address_get(["invoice"]) + if partner.free_member: + raise UserError(_("Partner is a free Member.")) + if not addr.get("invoice", False): + raise UserError( + _("Partner doesn't have an address to make the invoice.") + ) + invoice_vals_list.append( + partner.prepare_membership_invoice_vals(product, amount) + ) + + return self.env["account.move"].create(invoice_vals_list) diff --git a/membership_initial_discount/views/product_template_views.xml b/membership_initial_discount/views/product_template_views.xml new file mode 100644 index 0000000..4f03a38 --- /dev/null +++ b/membership_initial_discount/views/product_template_views.xml @@ -0,0 +1,18 @@ + + + + Membership Products (initial discount) + product.template + + + + + + + + + + + + + \ No newline at end of file