diff --git a/.gitignore b/.gitignore index 68bc17f..016aa55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.aider* # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/bemade_odoo_partner_scrapper/__manifest__.py b/bemade_odoo_partner_scrapper/__manifest__.py index 39d426f..9e389e0 100644 --- a/bemade_odoo_partner_scrapper/__manifest__.py +++ b/bemade_odoo_partner_scrapper/__manifest__.py @@ -16,10 +16,11 @@ 'license': 'GPL-3', 'author': 'Bemade', 'website': 'https://www.bemade.org', - 'depends': [], + 'depends': ['base', 'contacts', 'sale_management', 'sale' , 'partner_multi_relation'], 'data': [ # No security 'views/res_partner_views.xml', + 'data/res_partner_relation_type.xml' ], "assets": { "web.assets_backend": [ diff --git a/bemade_odoo_partner_scrapper/data/demo.xml b/bemade_odoo_partner_scrapper/data/demo.xml new file mode 100644 index 0000000..c47c023 --- /dev/null +++ b/bemade_odoo_partner_scrapper/data/demo.xml @@ -0,0 +1,106 @@ + + + + + + Is assistant of + Has assistant + p + p + + + Is competitor of + Is competitor of + c + c + + + + Has worked for + Has former employee + p + c + + + + Washing Companies + + + Washing Gold + + + + Washing Silver + + + + Washing Services + + + + + Great Washing Powder Company + + 1 + Le Bourget du Lac + 73377 + +33 4 49 23 44 54 + + 93, Press Avenue + great@yourcompany.example.com + http://www.great.com + + + Best Washing Powder Company + + 1 + Champs sur Marne + 77420 + + best@yourcompany.example.com + +33 1 64 61 04 01 + 12 rue Albert Einstein + http://www.best.com/ + + + Super Washing Powder Company + + 1 + 3rd Floor, Room 3-C, + Carretera Panamericana, Km 1, Urb. Delgado Chalbaud + Caracas + 1090 + super@yourcompany.example.com + +58 212 681 0538 + + super.com + + + + + + + + + + + + + + + + + + diff --git a/bemade_odoo_partner_scrapper/data/res_partner_relation_type.xml b/bemade_odoo_partner_scrapper/data/res_partner_relation_type.xml new file mode 100644 index 0000000..b193a2d --- /dev/null +++ b/bemade_odoo_partner_scrapper/data/res_partner_relation_type.xml @@ -0,0 +1,36 @@ + + + + + + Is Odoo Partner Of + Is Odoo Client Of + c + c + + + + + Odoo Partner + + + Learning Partner + + + + Ready Partner + + + + Silver Partner + + + + Gold Partner + + + diff --git a/bemade_odoo_partner_scrapper/models/res_partner.py b/bemade_odoo_partner_scrapper/models/res_partner.py index c341589..64983a0 100644 --- a/bemade_odoo_partner_scrapper/models/res_partner.py +++ b/bemade_odoo_partner_scrapper/models/res_partner.py @@ -1,16 +1,85 @@ from odoo import models, fields, api, _ import requests from bs4 import BeautifulSoup +import logging + +_logger = logging.getLogger(__name__) class ResPartner(models.Model): _inherit = 'res.partner' - is_odoo_partner = fields.Boolean(string="Is Odoo Partner", default=False) - is_odoo_user = fields.Boolean(string="Is Odoo User", default=False) + is_odoo_partner = fields.Boolean(string="Is Odoo Partner", default=False, tracking=True) + is_odoo_user = fields.Boolean(string="Is Odoo User", default=False, tracking=True) + odoo_name = fields.Char(string="Name on Odoo", tracking=True) + odoo_id = fields.Char(string="ID on Odoo", tracking=True) + odoo_url = fields.Char(string="Odoo Partner URL", tracking=True, index=True) @api.model def get_odoo_partner(self): + + def extract_partner_data(url): + page = requests.get("https://www.odoo.com" + url) + soup = BeautifulSoup(page.content, 'html.parser') + + partner_data = {} + + partner_data['name'] = soup.find(id="partner_name").text + + partner_type = soup.find('h3', class_='col-lg-12 text-center text-muted') + if partner_type and partner_type.span: + partner_data['partner_type'] = partner_type.span.text.strip() + + partner_data['odoo_name'] = soup.find(id="partner_name").text + address_div = soup.find('span', itemprop='streetAddress') + email_div = soup.find('span', itemprop='email') + phone_div = soup.find('span', itemprop='telephone') + website_div = soup.find('span', itemprop='website') + state_data = None + + if address_div is not None: + # Extract address components + address_parts = address_div.get_text(separator="\n").split("\n") + partner_data['street'] = address_parts[0] + city_state_postal = address_parts[len(address_parts) - 2].split(', ') + + if city_state_postal != [',']: + partner_data['city'] = city_state_postal[0] + state_postal = city_state_postal[1].split(' ') + if len(state_postal) == 3 and len(state_postal[2]) == 3: + partner_data['zip'] = state_postal[1] + state_postal[2] + state_data = state_postal[0] + elif len(state_postal) == 2 and state_postal[1]: + if len(state_postal[1]) == 6: + partner_data['zip'] = state_postal[1] + state_data = state_postal[0] + if len(state_postal[1]) == 3: + if len(state_postal[0]) == 2: + partner_data['zip'] = state_postal[1] + state_data = state_postal[0] + else: + partner_data['zip'] = state_postal[0] + state_postal[1] + else: + if len(state_postal[0]) == 6: + partner_data['zip'] = state_postal[0] + else: + state_data = state_postal[0] + + # partner_data['country'] = address_parts[len(address_parts) - 1] + + partner_data['email'] = email_div.string if email_div and email_div.string else '' + partner_data['phone'] = phone_div.string if phone_div and phone_div.string else '' + partner_data['website'] = website_div.string if website_div and website_div.string else '' + partner_data['is_odoo_partner'] = True + partner_data['is_odoo_user'] = True + + if state_data: + print(state_data) + country_id = self.env['res.country'].search([('code', '=', 'CA')]).id + partner_data['state_id'] = self.env['res.country.state'].search([('code', '=', state_data),('country_id', '=', country_id)]).id + + return partner_data + # Load the webpage at the given url page = requests.get("https://www.odoo.com/fr_FR/partners/country/canada-36") # Parse the HTML content @@ -50,37 +119,90 @@ class ResPartner(models.Model): for partner_url in partners_url: page = requests.get("https://www.odoo.com" + partner_url) soup = BeautifulSoup(page.content, 'html.parser') - partner_name = soup.find(id="partner_name") - address_div = soup.find('span', itemprop='streetAddress') - email_div = soup.find('span', itemprop='email') - phone_div = soup.find('span', itemprop='telephone') - website_div = soup.find('span', itemprop='website') - if address_div is not None: - # Extract address components - address_parts = address_div.get_text(separator="\n").split("\n") - street_address = address_parts[0] - city_state_postal = address_parts[len(address_parts)-2].split(', ') - city = city_state_postal[0] - state_postal = city_state_postal[1].split(' ') - state = state_postal[0] - postal_code = state_postal[1] + state_postal[2] - country = address_parts[len(address_parts)-1] - if email_div and email_div.string: - email = email_div.string - if phone_div and phone_div.string: - phone = phone_div.string - if website_div and website_div.string: - website = website_div.string - # self.env['res.partner'].create({ - # 'name': partner_name, - # 'street': street_address, - # 'city': city, - # 'state_id': state, - # 'zip': postal_code, - # 'country_id': country, - # 'email': email, - # 'phone': phone, - # 'website': website, - # 'is_odoo_partner': True, - # }) \ No newline at end of file + print (partner_url) + odoo_partner = extract_partner_data(partner_url) + print (odoo_partner) + + # partner_name = soup.find(id="partner_name").text + # address_div = soup.find('span', itemprop='streetAddress') + # email_div = soup.find('span', itemprop='email') + # phone_div = soup.find('span', itemprop='telephone') + # website_div = soup.find('span', itemprop='website') + # if address_div is not None: + # # Extract address components + # address_parts = address_div.get_text(separator="\n").split("\n") + # street_address = address_parts[0] + # city_state_postal = address_parts[len(address_parts) - 2].split(', ') + # + # if city_state_postal != [',']: + # city = city_state_postal[0] + # state_postal = city_state_postal[1].split(' ') + # + # if len(state_postal) == 3 and len(state_postal[2]) == 3: + # postal_code = state_postal[1] + state_postal[2] + # state = state_postal[0] + # elif len(state_postal) == 2 and state_postal[1]: + # if len(state_postal[1]) == 6: + # postal_code = state_postal[1] + # state = state_postal[0] + # if len(state_postal[1]) == 3: + # if len(state_postal[0]) == 2: + # postal_code = state_postal[1] + # state = state_postal[0] + # else: + # postal_code = state_postal[0] + state_postal[1] + # state = '' + # else: + # if len(state_postal[0]) == 6: + # postal_code = state_postal[0] + # state = '' + # else: + # postal_code = '' + # state = state_postal[0] + # else: + # city = '' + # state = '' + # postal_code = '' + # # state = state_postal[0] + # # postal_code = state_postal[1] + state_postal[2] + # country = address_parts[len(address_parts) - 1] + # if email_div and email_div.string: + # email = email_div.string + # else: + # email = '' + # if phone_div and phone_div.string: + # phone = phone_div.string + # else: + # phone = '' + # if website_div and website_div.string: + # website = website_div.string + # else: + # website = '' + + _logger.info(f"Processing {odoo_partner['name']} with email {odoo_partner['email']} and phone {odoo_partner['phone']} and website {odoo_partner['website']}") + + exist_odoo_name = self.env['res.partner'].search([('odoo_name', '=', odoo_partner['odoo_name'])]) + if not exist_odoo_name: + exist_email = self.env['res.partner'].search([('email', '=', odoo_partner['email'])]) + if not exist_email: + exist_phone = self.env['res.partner'].search([('phone', '=', odoo_partner['phone'])]) + if not exist_phone: + exist_website = self.env['res.partner'].search([('website', '=', odoo_partner['website'])]) + + if not exist_website or odoo_partner['website'] == '': + if odoo_partner['website'] == '': + del odoo_partner['website'] + self.env['res.partner'].create(odoo_partner) + # self.env['res.partner'].create({ + # 'name': partner_name, + # 'street': street_address, + # 'city': city, + # 'state_id': state_id.id if state_id else False, + # 'zip': postal_code, + # 'country_id': country_id.id if country_id else False, + # 'email': email, + # 'phone': phone, + # 'website': website, + # 'is_odoo_partner': True, + # }) \ No newline at end of file diff --git a/bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js b/bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js index 6f7f7c0..f3b0ba6 100644 --- a/bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js +++ b/bemade_odoo_partner_scrapper/static/src/js/odoo_scrapper.js @@ -2,6 +2,8 @@ import ListController from 'web.ListController'; import ListView from 'web.ListView'; +import KanbanController from 'web.KanbanController'; +import KanbanView from 'web.KanbanView'; const viewRegistry = require('web.view_registry'); const OdooScrapperListController = ListController.extend({ @@ -32,3 +34,38 @@ const OdooScrapperListView = ListView.extend({ // key must match with the js_class attribute of the tree view you want to modify viewRegistry.add('res_partner_odoo_scrapper_tree', OdooScrapperListView); + + + + + +const OdooScrapperKanbanController = KanbanController.extend({ + // buttons_template must match the t-name on the template for the button (static xml) + buttons_template: 'odoo_scrapper.kanban_view_buttons', + events: _.extend({}, KanbanController.prototype.events, { + 'click .o_button_get_partner': '_onGetPartnerClick', + }), + // This may need to be async function() if there needs to be an await this._rpc({ ... }); call to not reload early + _onGetPartnerClick: function () { + this._rpc({ + model: 'res.partner', + method: 'get_odoo_partner', + args: [], + }).then(() => { + this.reload(); + }); + // Couldn't test this for real, but it runs the action. May need a this.reload() + }, +}); + +const OdooScrapperKanbanView = KanbanView.extend({ + config: _.extend({}, KanbanView.prototype.config, { + Controller: OdooScrapperKanbanController, + }), +}); + +// key must match with the js_class attribute of the tree view you want to modify +viewRegistry.add('res_partner_odoo_scrapper_kanban', OdooScrapperKanbanView); + + + diff --git a/bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml b/bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml index 1a33faa..942044b 100644 --- a/bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml +++ b/bemade_odoo_partner_scrapper/static/src/xml/odoo_scrapper_templates.xml @@ -1,12 +1,24 @@ - + + title="Get Odoo Partner"> Get Odoo Partner + + + + + + + + + + \ No newline at end of file diff --git a/bemade_odoo_partner_scrapper/views/res_partner_views.xml b/bemade_odoo_partner_scrapper/views/res_partner_views.xml index 10a55ed..1dd2f6e 100644 --- a/bemade_odoo_partner_scrapper/views/res_partner_views.xml +++ b/bemade_odoo_partner_scrapper/views/res_partner_views.xml @@ -15,4 +15,27 @@ + + res.partner.kanban.inherit + res.partner + + + + res_partner_odoo_scrapper_kanban + + + + + + res.partner.odoo.search + res.partner + + + + + + + + + diff --git a/bemade_time_off_follower/__init__.py b/bemade_time_off_follower/__init__.py new file mode 100644 index 0000000..9a7e03e --- /dev/null +++ b/bemade_time_off_follower/__init__.py @@ -0,0 +1 @@ +from . import models \ No newline at end of file diff --git a/bemade_time_off_follower/__manifest__.py b/bemade_time_off_follower/__manifest__.py new file mode 100644 index 0000000..76eea2d --- /dev/null +++ b/bemade_time_off_follower/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +{ + "name": "Time Off Alternative Follower", + "version": "15.0.0.0.1", + "category": "Extra Tools", + 'summary': 'Add Alternative Follower When Receiving Message While On Time Off', + "description": """ + Add Alternative Follower When Receiving Message While On Time Off + """, + "author": "Bemade", + 'website': 'https://www.bemade.org', + "depends": [ + 'hr_holidays', + 'mail' + ], + "data": [ + 'views/hr_leave_views.xml', + ], + "auto_install": False, + "installable": True, + 'license': 'OPL-1' +} diff --git a/bemade_time_off_follower/models/__init__.py b/bemade_time_off_follower/models/__init__.py new file mode 100644 index 0000000..fa00ea0 --- /dev/null +++ b/bemade_time_off_follower/models/__init__.py @@ -0,0 +1,2 @@ +from . import hr_leave +from . import mail_thread diff --git a/bemade_time_off_follower/models/hr_leave.py b/bemade_time_off_follower/models/hr_leave.py new file mode 100644 index 0000000..09750a0 --- /dev/null +++ b/bemade_time_off_follower/models/hr_leave.py @@ -0,0 +1,10 @@ +from odoo import models, fields, api + +class HrLeave(models.Model): + _inherit = 'hr.leave' + + alternate_follower_id = fields.Many2one( + comodel_name='res.users', + string='Alternate Follower', + help='User who will receive a copy of all communications related to this leave', + ) diff --git a/bemade_time_off_follower/models/mail_thread.py b/bemade_time_off_follower/models/mail_thread.py new file mode 100644 index 0000000..661961a --- /dev/null +++ b/bemade_time_off_follower/models/mail_thread.py @@ -0,0 +1,26 @@ +from odoo import models, api + +class MailThread(models.AbstractModel): + _inherit = 'mail.thread' + + @api.returns('mail.message', lambda value: value.id) + def message_post(self, **kwargs): + message = super(MailThread, self).message_post(**kwargs) + + leave_env = self.env['hr.leave'] + for recipient in message.notification_ids.mapped('res_partner_id'): + leave = leave_env.search([ + ('state', '=', 'validate'), + ('date_from', '<=', fields.Date.today()), + ('date_to', '>=', fields.Date.today()), + ('employee_id.user_id.partner_id', '=', recipient.id), + ], limit=1) + + if leave and leave.alternate_follower_id: + message.write({ + 'notification_ids': [(0, 0, { + 'res_partner_id': leave.alternate_follower_id.partner_id.id, + # 'notification_type': 'email', + })] + }) + return message diff --git a/bemade_time_off_follower/views/hr_leave_views.xml b/bemade_time_off_follower/views/hr_leave_views.xml new file mode 100644 index 0000000..a992f48 --- /dev/null +++ b/bemade_time_off_follower/views/hr_leave_views.xml @@ -0,0 +1,13 @@ + + + + hr.leave.form.view.inherit + hr.leave + + + + + + + +