diff --git a/membership_product_set/README.rst b/membership_product_set/README.rst new file mode 100644 index 00000000..8f775038 --- /dev/null +++ b/membership_product_set/README.rst @@ -0,0 +1,58 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl + :alt: License: AGPL-3 + +=============================================== +Membership Set +=============================================== + +This module enable the user to define a Set of Memberships and +create the summarized invoice. + + +Usage +===== + +The module adds a boolean field "Membership set" in model "product.template". +Once it was set, the User can define the membership set by adding the normal +memberships in field "Membership products". + +The module extends the wizard "Buy membership" for creating +a summarized invoice, which contains the invoice lines for each +membership in the selected Membership set. + +Known issues / Roadmap +====================== + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Yu Weng + +Do not contact contributors directly about support or help with technical issues. + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/membership_product_set/__init__.py b/membership_product_set/__init__.py new file mode 100644 index 00000000..3c5cb3bf --- /dev/null +++ b/membership_product_set/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Yu Weng +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from . import models diff --git a/membership_product_set/__manifest__.py b/membership_product_set/__manifest__.py new file mode 100644 index 00000000..55359709 --- /dev/null +++ b/membership_product_set/__manifest__.py @@ -0,0 +1,19 @@ +# Copyright 2019 Yu Weng +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + 'name': 'Product Set for memberships', + 'version': '11.0.1.0.0', + 'license': 'AGPL-3', + 'category': 'Association', + 'author': 'Yu Weng, ' + 'Odoo Community Association (OCA)', + 'website': 'https://github.com/OCA/vertical-association', + 'depends': [ + 'membership', + ], + 'data': [ + 'views/product_template_views.xml', + ], + "installable": True, +} diff --git a/membership_product_set/i18n/de.po b/membership_product_set/i18n/de.po new file mode 100644 index 00000000..a8e02282 --- /dev/null +++ b/membership_product_set/i18n/de.po @@ -0,0 +1,51 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * membership_product_set +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 11.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2019-04-18 00:20+0000\n" +"PO-Revision-Date: 2019-04-18 00:20+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: membership_product_set +#: model:ir.model.fields,help:membership_product_set.field_product_product_membership_set +#: model:ir.model.fields,help:membership_product_set.field_product_template_membership_set +msgid "Check if the product is eligible for membership set." +msgstr "Überprüfen Sie, ob das Produkt als Mitgliedschaften angelegt wurde." + +#. module: membership_product_set +#: model:ir.model.fields,field_description:membership_product_set.field_product_product_membership_set_products +#: model:ir.model.fields,field_description:membership_product_set.field_product_template_membership_set_products +msgid "Membership products" +msgstr "Mitgliedschaft Produkte" + +#. module: membership_product_set +#: model:ir.model.fields,field_description:membership_product_set.field_product_product_membership_set +#: model:ir.model.fields,field_description:membership_product_set.field_product_template_membership_set +msgid "Membership set" +msgstr "Mitgliedschaften" + +#. module: membership_product_set +#: code:addons/membership_product_set/models/res_partner.py:27 +#, python-format +msgid "Partner doesn't have an account to make the invoice." +msgstr "Partner hat kein Debitorenkonto für Rechnung" + +#. module: membership_product_set +#: code:addons/membership_product_set/models/res_partner.py:25 +#, python-format +msgid "Partner is a free Member." +msgstr "Partner ist freies Mitglied" + +#. module: membership_product_set +#: model:ir.model,name:membership_product_set.model_product_template +msgid "Product Template" +msgstr "Produktvorlage" diff --git a/membership_product_set/models/__init__.py b/membership_product_set/models/__init__.py new file mode 100644 index 00000000..8fd348aa --- /dev/null +++ b/membership_product_set/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2019 Yu Weng +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from . import product_template +from . import res_partner diff --git a/membership_product_set/models/product_template.py b/membership_product_set/models/product_template.py new file mode 100644 index 00000000..6cd13ad9 --- /dev/null +++ b/membership_product_set/models/product_template.py @@ -0,0 +1,18 @@ +# Copyright 2019 Yu Weng +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import fields, models + + +class ProductTemplate(models.Model): + _inherit = "product.template" + + membership_set = fields.Boolean( + help="Check if the product is eligible for membership set.", + string="Membership set") + membership_set_products = fields.Many2many( + 'product.product', + 'membership_set_products_rel', + 'membership_set_id', + 'product_id', + string="Membership products") diff --git a/membership_product_set/models/res_partner.py b/membership_product_set/models/res_partner.py new file mode 100644 index 00000000..200f4a8c --- /dev/null +++ b/membership_product_set/models/res_partner.py @@ -0,0 +1,58 @@ +# Copyright 2019 Yu Weng +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from odoo import api, models, _ +from odoo.exceptions import UserError + + +class ResPartner(models.Model): + _inherit = "res.partner" + + @api.multi + def create_membership_invoice(self, product_id=None, datas=None): + product_id = product_id or datas.get('membership_product_id') + amount = datas.get('amount', 0.0) + product = self.env['product.product'].browse(product_id) + invoice_list = [] + if product.membership_set: + for partner in self: + if partner.free_member: + raise UserError(_("Partner is a free Member.")) + account_id = partner.property_account_receivable_id.id + w = _("Partner doesn't have an account to make the invoice.") + if not account_id: + raise UserError(w) + position_id = partner.property_account_position_id.id + invoice = self.env['account.invoice'].create({ + 'partner_id': partner.id, + 'account_id': account_id, + 'fiscal_position_id': position_id + }) + for p in product.membership_set_products: + price_dict = p.price_compute('list_price') + amount = price_dict.get(p.id) or 0 + line_values = { + 'product_id': p.id, + 'price_unit': amount, + 'invoice_id': invoice.id, + } + + # create a record in cache, apply onchange + # then revert back to a dictionnary + invoice_line = self.env['account.invoice.line']\ + .new(line_values) + invoice_line._onchange_product_id() + line_values = invoice_line._convert_to_write({ + name: invoice_line[name] + for name in invoice_line._cache + }) + line_values['price_unit'] = amount + invoice.write({'invoice_line_ids': [(0, 0, line_values)]}) + invoice_list.append(invoice.id) + invoice.compute_taxes() + else: + invoice_list = super(ResPartner, self).create_membership_invoice( + product_id=product_id, + datas=datas + ) + return invoice_list diff --git a/membership_product_set/static/description/icon.png b/membership_product_set/static/description/icon.png new file mode 100644 index 00000000..3a0328b5 Binary files /dev/null and b/membership_product_set/static/description/icon.png differ diff --git a/membership_product_set/static/description/icon.svg b/membership_product_set/static/description/icon.svg new file mode 100644 index 00000000..6fb22c9d --- /dev/null +++ b/membership_product_set/static/description/icon.svg @@ -0,0 +1,455 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/membership_product_set/static/description/icon_main.svg b/membership_product_set/static/description/icon_main.svg new file mode 100644 index 00000000..c7f5f0a9 --- /dev/null +++ b/membership_product_set/static/description/icon_main.svg @@ -0,0 +1,735 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/membership_product_set/tests/__init__.py b/membership_product_set/tests/__init__.py new file mode 100644 index 00000000..0a6aac5f --- /dev/null +++ b/membership_product_set/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Yu Weng +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from . import test_membership diff --git a/membership_product_set/tests/test_membership.py b/membership_product_set/tests/test_membership.py new file mode 100644 index 00000000..28502078 --- /dev/null +++ b/membership_product_set/tests/test_membership.py @@ -0,0 +1,127 @@ +# Copyright 2019 Yu Weng +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +from datetime import timedelta +from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import common + + +class TestMembership(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestMembership, cls).setUpClass() + date_today = fields.Date.from_string(fields.Date.today()) + cls.next_month = fields.Date.to_string(date_today + timedelta(days=30)) + cls.tax_15p = cls.env['account.tax'].create({ + 'type_tax_use': 'sale', + 'amount_type': 'percent', + 'name': "Tax 15%", + 'amount': 15, + 'active': True, + }) + cls.account_partner_type = cls.env['account.account.type'].create({ + 'name': 'Test partner account type', + 'type': 'receivable', + }) + cls.account_partner = cls.env['account.account'].create({ + 'name': 'Test partner account', + 'code': 'PARTNER', + 'user_type_id': cls.account_partner_type.id, + 'reconcile': True, + }) + cls.partner = cls.env['res.partner'].create({ + 'name': 'Test partner', + 'free_member': True, + 'property_account_receivable_id': cls.account_partner.id, + }) + cls.partner_wo_account = cls.env['res.partner'].create({ + 'name': 'Test Contact WO Account', + 'property_account_receivable_id': False, + }) + cls.gold_product = cls.env['product.product'].create({ + 'type': 'service', + 'name': 'Membership Gold', + 'membership': True, + 'membership_date_from': fields.Date.today(), + 'membership_date_to': cls.next_month, + 'list_price': 100.00, + 'taxes_id': [(4, cls.tax_15p.id, False)] + }) + cls.silver_product = cls.env['product.product'].create({ + 'type': 'service', + 'name': 'Membership Silver', + 'membership': True, + 'membership_date_from': fields.Date.today(), + 'membership_date_to': cls.next_month, + 'list_price': 50.00, + }) + cls.basic_product = cls.env['product.product'].create({ + 'type': 'service', + 'name': 'Membership Basic', + 'membership': True, + 'membership_date_from': fields.Date.today(), + 'membership_date_to': cls.next_month, + 'list_price': 20.00, + }) + cls.product_set = cls.env['product.product'].create({ + 'type': 'service', + 'name': 'Membership Set', + 'membership': True, + 'membership_set': True, + 'membership_date_from': fields.Date.today(), + 'membership_date_to': cls.next_month, + 'list_price': 150.00, + 'membership_set_products': [ + (4, cls.gold_product.id, False), + (4, cls.silver_product.id, False), + ] + }) + + def test_membership_invoice(self): + wizard = self.env['membership.invoice'].with_context( + active_ids=[self.partner.id], + active_model='res.partner' + ).create({ + 'product_id': self.product_set.id, + 'member_price': 150, + }) + with self.assertRaises(UserError): + invoice_id = wizard.membership_invoice()['domain'][0][2] + + self.partner.free_member = False + invoice_id = wizard.membership_invoice()['domain'][0][2] + invoice = self.env['account.invoice'].browse(invoice_id) + for line in invoice.invoice_line_ids: + if line.product_id.id == self.gold_product.id: + self.assertEqual(line.price_unit, 100) + self.assertEqual( + line.invoice_line_tax_ids[0].id, + self.tax_15p.id) + elif line.product_id.id == self.silver_product.id: + self.assertEqual(line.price_unit, 50) + self.assertEqual(len(line.invoice_line_tax_ids), 0) + else: + self.assertTrue(False) + self.assertEqual(invoice.amount_untaxed, 150) + self.assertEqual(invoice.amount_tax, 15) + self.assertEqual(invoice.amount_total, 165) + + wizard = self.env['membership.invoice'].with_context( + active_ids=[self.partner_wo_account.id], + active_model='res.partner' + ).create({ + 'product_id': self.product_set.id, + 'member_price': 150, + }) + with self.assertRaises(UserError): + invoice_id = wizard.membership_invoice()['domain'][0][2] + + wizard = self.env['membership.invoice'].with_context( + active_ids=[self.partner.id], + active_model='res.partner' + ).create({ + 'product_id': self.basic_product.id, + 'member_price': 100, + }) + wizard.membership_invoice() diff --git a/membership_product_set/views/product_template_views.xml b/membership_product_set/views/product_template_views.xml new file mode 100644 index 00000000..3986dcf3 --- /dev/null +++ b/membership_product_set/views/product_template_views.xml @@ -0,0 +1,20 @@ + + + + + + Membership Product set + product.template + + + + + + + + + +