diff --git a/delivery_package_fee/tests/test_package_fee.py b/delivery_package_fee/tests/test_package_fee.py index e0e7df50d6..fb0f9d4a6d 100644 --- a/delivery_package_fee/tests/test_package_fee.py +++ b/delivery_package_fee/tests/test_package_fee.py @@ -154,6 +154,16 @@ def test_package_fee_simple_with_fiscal_position_tax(self): "include_base_amount": True, } ) + tax_price_include2 = self.env["account.tax"].create( + { + "name": "15% inc", + "type_tax_use": "sale", + "amount_type": "percent", + "amount": 15, + "price_include": True, + "include_base_amount": True, + } + ) tax_price_exclude = self.env["account.tax"].create( { "name": "15% exc", @@ -180,6 +190,7 @@ def test_package_fee_simple_with_fiscal_position_tax(self): ) # Setting tax in fiscal position on fee2 product + self.fee1.taxes_id = tax_price_include2 self.fee2.taxes_id = tax_price_include self.sale.fiscal_position_id = fiscal_position diff --git a/delivery_roulier_geodis_fr/README.rst b/delivery_roulier_geodis_fr/README.rst new file mode 100644 index 0000000000..90053519f7 --- /dev/null +++ b/delivery_roulier_geodis_fr/README.rst @@ -0,0 +1,47 @@ +Delivery Carrier Geodis +======================= + + +Description +----------- +Send parcels with Geodis. +Labels are generated from WebServices. +Edi file is generated locally and should be sent +by another module. + +Glossary +-------- + +Agency: Geodis's hub your warehouse depends upon. + +Configuration + +## Create a partner for your agency. + +This modules comes with only one partner "Geodis". It's the head quarters of Geodis. +You need to create partners for the agency you depends : +- create a sub contact of "Geodis HQ", +- pay attention to fill correctly name, streets, phone, zip code, country and *SIRET* +- fill "ref" (internal reference) field with the agency id. + + +Features: +- Multiple Agencies. + +Known Issues: +~~~~~~~~~~~~~ + +- each pack is sent on his own : no handling of numbers of picking + + +Technical references +-------------------- + +'Geodis documentation: www.geodis.fr' + +Contributors +------------ + +* Raphaël REVERDY +* Eric Bouhana + diff --git a/delivery_roulier_geodis_fr/__init__.py b/delivery_roulier_geodis_fr/__init__.py new file mode 100644 index 0000000000..0650744f6b --- /dev/null +++ b/delivery_roulier_geodis_fr/__init__.py @@ -0,0 +1 @@ +from . import models diff --git a/delivery_roulier_geodis_fr/__manifest__.py b/delivery_roulier_geodis_fr/__manifest__.py new file mode 100644 index 0000000000..5d2dc42bd6 --- /dev/null +++ b/delivery_roulier_geodis_fr/__manifest__.py @@ -0,0 +1,29 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Delivery Carrier Geodis (fr)", + "version": "16.0.1.0.0", + "author": "Akretion, Odoo Community Association (OCA)", + "summary": "Generate Label for Geodis logistic", + "maintainers": ["florian-dacosta"], + "category": "Warehouse", + "depends": [ + "delivery_roulier", + "delivery_carrier_agency", + "delivery_carrier_deposit", + "delivery_roulier_option", + "partner_address_split", + "l10n_fr_siret", + ], + "website": "https://github.com/OCA/delivery-carrier", + "data": [ + "data/product.xml", + "data/delivery.xml", + "data/sequence_geodis.xml", + "views/carrier_account_views.xml", + "views/delivery_carrier_agency_views.xml", + ], + "demo": [], + "installable": True, + "license": "AGPL-3", +} diff --git a/delivery_roulier_geodis_fr/data/delivery.xml b/delivery_roulier_geodis_fr/data/delivery.xml new file mode 100644 index 0000000000..c386d80ec7 --- /dev/null +++ b/delivery_roulier_geodis_fr/data/delivery.xml @@ -0,0 +1,130 @@ + + + + + + Geodis + + 26 Quai Charles Pasqua + 92300 + +33156762600 + Levallois-Perret + + + + + Geodis Lille Europe + + 7 Avenue de la Rotonde + 59160 + +33320085555 + + Lomme + 457507358 + 00044 + + + + + France Express Lille + + 2 RUE DES SAPINS + 59810 + +336320161718 + LESQUIN + 300089174 + + 00035 + + + + + On Demand + RDW + + + + + Geodis Express + EXP + geodis_fr + + + + + Geodis Rapide + geodis_fr + + RAP + + + + Geodis Messagerie + geodis_fr + + MES + + + Geodis Top24 + geodis_fr + + T24 + + + + Geodis Calpack + geodis_fr + + CAL + + + + Geodis Pack30 + geodis_fr + + P30 + + + + Geodis InterPack + geodis_fr + + INP + + + + Geodis Messagerie Internationnale + geodis_fr + + MEI + + + + Geodis CXI France Express + geodis_fr + + CXI + + + + Geodis CX Expres + geodis_fr + + CX + + + + Geodis Inter Express + geodis_fr + + INE + + + + Geodis Euro Express + geodis_fr + + EEX + + + diff --git a/delivery_roulier_geodis_fr/data/product.xml b/delivery_roulier_geodis_fr/data/product.xml new file mode 100644 index 0000000000..d260d97f82 --- /dev/null +++ b/delivery_roulier_geodis_fr/data/product.xml @@ -0,0 +1,10 @@ + + + + + SHIP_GEODIS + service + Coûts de livraison - GEODIS + + + diff --git a/delivery_roulier_geodis_fr/data/sequence_geodis.xml b/delivery_roulier_geodis_fr/data/sequence_geodis.xml new file mode 100644 index 0000000000..cb0314efe9 --- /dev/null +++ b/delivery_roulier_geodis_fr/data/sequence_geodis.xml @@ -0,0 +1,9 @@ + + + Numerotation des colis geodis + geodis.nrecep.number + %(year)s_ + 8 + no_gap + + diff --git a/delivery_roulier_geodis_fr/models/__init__.py b/delivery_roulier_geodis_fr/models/__init__.py new file mode 100644 index 0000000000..f550ad0bc9 --- /dev/null +++ b/delivery_roulier_geodis_fr/models/__init__.py @@ -0,0 +1,6 @@ +from . import stock_picking +from . import delivery_carrier +from . import carrier_account +from . import delivery_carrier_agency +from . import deposit +from . import stock_quant_package diff --git a/delivery_roulier_geodis_fr/models/carrier_account.py b/delivery_roulier_geodis_fr/models/carrier_account.py new file mode 100644 index 0000000000..51301b5b00 --- /dev/null +++ b/delivery_roulier_geodis_fr/models/carrier_account.py @@ -0,0 +1,16 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class CarrierAccount(models.Model): + _inherit = "carrier.account" + + geodis_fr_customer_id = fields.Char(string="Customer Id") + geodis_fr_file_format = fields.Selection( + [("ZPL", "ZPL")], default="ZPL", string="Geodis File Format" + ) + geodis_fr_tracking_account = fields.Boolean( + string="Is a Tracking Account", + help="Check this box if this account is used to get the tracking links for geodis", + ) diff --git a/delivery_roulier_geodis_fr/models/delivery_carrier.py b/delivery_roulier_geodis_fr/models/delivery_carrier.py new file mode 100644 index 0000000000..b38918e76d --- /dev/null +++ b/delivery_roulier_geodis_fr/models/delivery_carrier.py @@ -0,0 +1,12 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class DeliveryCarrier(models.Model): + _inherit = "delivery.carrier" + + delivery_type = fields.Selection( + selection_add=[("geodis_fr", "Geodis")], + ondelete={"geodis_fr": "set default"}, + ) diff --git a/delivery_roulier_geodis_fr/models/delivery_carrier_agency.py b/delivery_roulier_geodis_fr/models/delivery_carrier_agency.py new file mode 100644 index 0000000000..ac1bbf8b66 --- /dev/null +++ b/delivery_roulier_geodis_fr/models/delivery_carrier_agency.py @@ -0,0 +1,11 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import fields, models + + +class DeliveryCarrierAgency(models.Model): + _inherit = "delivery.carrier.agency" + + geodis_fr_interchange_sender = fields.Char() + geodis_fr_interchange_recipient = fields.Char() + geodis_fr_hub_id = fields.Char() diff --git a/delivery_roulier_geodis_fr/models/deposit.py b/delivery_roulier_geodis_fr/models/deposit.py new file mode 100644 index 0000000000..a3507e9b3f --- /dev/null +++ b/delivery_roulier_geodis_fr/models/deposit.py @@ -0,0 +1,147 @@ +import logging +from base64 import b64encode + +from odoo import models +from odoo.exceptions import UserError +from odoo.tools.translate import _ + +_logger = logging.getLogger(__name__) + +try: + from roulier import roulier + from roulier.exception import InvalidApiInput # CarrierError +except ImportError: + _logger.debug("Cannot `import roulier`.") + + +class DepositSlip(models.Model): + _inherit = "deposit.slip" + + def _geodis_prepare_data(self): + """Create lines for each picking. + + In a EDI file order is important. + Returns a dict per agencies + + @returns [] + """ + self.ensure_one() + + # build a dict of pickings per agencies + def pickings_agencies(pickings): + agencies = {} + for picking in pickings: + agency = picking._get_carrier_agency() + agencies.setdefault(agency, {"senders": None, "pickings": []},)[ + "pickings" + ].append(picking) + return agencies + + pickagencies = pickings_agencies(self.picking_ids) + + # build a dict of pickings per sender + def pickings_senders(pickings): + senders = {} + for picking in pickings: + partner = picking._get_sender(None) + senders.setdefault( + partner.id, + {"pickings": [], "account": picking._get_account()}, + )["pickings"].append(picking) + return senders + + for _agency, pickagency in pickagencies.items(): + pickagency["senders"] = pickings_senders(pickagency["pickings"]) + + # build a response file per agency / sender + files = [] + i = 0 + for agency, pickagency in pickagencies.items(): + for sender_id, picksender in pickagency["senders"].items(): + i += 1 + + # consolidate pickings for agency / sender + shipments = [ + picking._geodis_fr_prepare_edi() + for picking in picksender["pickings"] + ] + # we need one of the pickings to lookup addresses + picking = picksender["pickings"][0] + from_address = self._geodis_get_from_address(picking) + agency_address = self._geodis_get_agency_address(picking, agency) + account = picksender["account"] + + service = { + "depositId": "%s%s" % (self.id, i), + "depositDate": self.create_date, + "customerId": account.geodis_fr_customer_id, + "interchangeSender": agency.geodis_fr_interchange_sender, + "interchangeRecipient": agency.geodis_fr_interchange_recipient, + } + files.append( + { + "shipments": shipments, + "from_address": from_address, + "agency_address": agency_address, + "service": service, + "agency_id": agency.external_reference, + "sender_id": sender_id, + } + ) + return files + + def _geodis_get_from_address(self, picking): + """Return a dict of the sender.""" + partner = picking._get_sender(None) + address = picking._convert_address(partner) + address["siret"] = partner.siret or "" + return address + + def _geodis_get_agency_address(self, picking, agency): + """Return a dict the agency.""" + partner = agency.partner_id + address = picking._convert_address(partner) + address["siret"] = partner.siret or "" + return address + + def _geodis_create_edi_file(self, payload): + """Create a edi file with headers and data. + + One agency per call. + + params: + payload : roulier.get_api("edi") + return: string + """ + try: + edi = roulier.get(self.delivery_type, "get_edi", payload) + except InvalidApiInput as e: + raise UserError(_("Bad input: %s\n") % str(e)) from e + return edi + + def _get_geodis_attachment_name(self, idx, payload_agency): + return "%s_%s.txt" % (self.name, idx) + + def _geodis_create_attachments(self): + """Create EDI files in attachment.""" + payloads = self._geodis_prepare_data() + attachments = self.env["ir.attachment"] + for idx, payload_agency in enumerate(payloads, start=1): + edi_file = self._geodis_create_edi_file(payload_agency) + file_name = self._get_geodis_attachment_name(idx, payload_agency) + vals = { + "name": file_name, + "res_id": self.id, + "res_model": "deposit.slip", + "datas": b64encode(edi_file.encode("utf8")), + "type": "binary", + } + attachments += self.env["ir.attachment"].create(vals) + return attachments + + def create_edi_file(self): + self.ensure_one() + if self.delivery_type == "geodis_fr": + return self._geodis_create_attachments() + else: + return super().create_edi_file() diff --git a/delivery_roulier_geodis_fr/models/stock_picking.py b/delivery_roulier_geodis_fr/models/stock_picking.py new file mode 100644 index 0000000000..0c548380a5 --- /dev/null +++ b/delivery_roulier_geodis_fr/models/stock_picking.py @@ -0,0 +1,230 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import _, fields, models +from odoo.exceptions import Warning as UserError + +_logger = logging.getLogger(__name__) +try: + from roulier import roulier + from roulier.exception import CarrierError, InvalidApiInput +except ImportError: + _logger.debug("Cannot `import roulier`.") + + +GEODIS_DEFAULT_PRIORITY = { + "MES": "3", + "MEI": "3", + "CXI": "1", + "CX": "1", + "EEX": "1", +} + +GEODIS_DEFAULT_TOD = { + "MES": "P", + "MEI": "DAP", + "CXI": "P", + "EEX": "DAP", +} + +ADDRESS_ERROR_CODES = ["C0041", "C0042", "C0044", "C0045", "C0047", "T0023"] + + +class StockPicking(models.Model): + _inherit = "stock.picking" + + geodis_shippingid = fields.Char( + help="Shipping Id in Geodis terminology", copy=False + ) + + def _geodis_fr_convert_address(self, partner): + """Truncate address and name to 35 chars.""" + address = self._roulier_convert_address(partner) or {} + # get_split_adress from partner_address_split module + streets = partner._get_split_address(3, 35) + address["street1"], address["street2"], address["street3"] = streets + for field in ("name", "city"): + address[field] = address[field][0:35] + return address + + def _geodis_fr_get_priority(self, package): + """Define options for the shippment.""" + return GEODIS_DEFAULT_PRIORITY.get(self.carrier_code, "") + + def _geodis_fr_get_options(self, package): + """Compact geodis options. + + Options are passed as string. it obey some custom + binding like RDW + AAO = AWO. + It should be implemented here. For the moment, only + one option can be passed. + """ + options = self._roulier_get_options(package) + actives = [option for option in options.keys() if options[option]] + return actives and actives[0] or "" + + def _geodis_fr_get_notifications(self, package): + options = self._get_options(package) + recipient = self._convert_address(self._get_receiver(package)) + if "RDW" in options: + if recipient.get("email"): + if recipient["phone"]: + return "M" + else: + return "P" + else: + if recipient.get("phone"): + return "S" + else: + raise UserError(_("Can't set up a rendez-vous wihout mail or tel")) + + def _geodis_fr_get_service(self, account, package=None): + service = self._roulier_get_service(account, package=package) + agency = self._get_carrier_agency() + + service["option"] = self._get_options(package) + service["notifications"] = self._geodis_fr_get_notifications(package) + service["customerId"] = account.geodis_fr_customer_id + service["agencyId"] = agency.external_reference + service["hubId"] = agency.geodis_fr_hub_id + self._gen_shipping_id() # explicit generation + service["shippingId"] = self.geodis_shippingid + return service + + def _geodis_fr_prepare_edi(self): + """Return a list.""" + self.ensure_one() + picking = self + + packages = picking.package_ids + parcels = [pack._get_edi_pack_vals() for pack in packages] + + return { + "product": picking.carrier_id.code, + "productOption": picking._get_options(None), + "productPriority": picking._geodis_fr_get_priority(None), + "notifications": picking._geodis_fr_get_notifications(None), + "productTOD": GEODIS_DEFAULT_TOD[picking.carrier_code], + "to_address": self._convert_address(picking._get_receiver(None)), + "reference1": picking.origin or picking.group_id.name or picking.name, + "reference2": "", + "reference3": "", + "shippingId": picking.geodis_shippingid, + "parcels": parcels, + } + + def _geodis_fr_get_address_proposition(self, raise_address=True): + # check address + self.ensure_one() + payload = {} + receiver = self._get_receiver() + account = self._get_account() + payload["auth"] = self._get_auth(account) + payload["to_address"] = self._convert_address(receiver) + payload["service"] = {"is_test": not self.carrier_id.prod_environment} + addresses = [] + try: + # api call + addresses = roulier.get(self.delivery_type, "validate_address", payload) + except InvalidApiInput as e: + raise UserError( + self.env["stock.quant.package"]._invalid_api_input_handling(payload, e) + ) from e + except CarrierError as e: + errors = e.args and e.args[0] + if ( + errors + and errors[0].get("id") + and not raise_address + and errors[0].get("id") in ADDRESS_ERROR_CODES + ): + return addresses + else: + package = self.env["stock.quant.package"].new({}) + package.carrier_id = self.carrier_id + raise UserError(package._carrier_error_handling(payload, e)) from e + return addresses + + def _geodis_fr_check_address(self): + self.ensure_one() + addresses = self._geodis_fr_get_address_proposition() + return len(addresses) == 1 + + def _gen_shipping_id(self): + """Generate a shipping id. + + Shipping id is persisted on the picking and it's + calculated from a sequence since it should be + 8 char long and unique for at least 1 year + """ + + def gen_id(): + sequence = self.env["ir.sequence"].next_by_code("geodis.nrecep.number") + # this is prefixed by year_ so we split it befor use + year, number = sequence.split("_") + # pad with 0 to build an 8digits number (string) + return "%08d" % int(number) + + for picking in self: + picking.geodis_shippingid = picking.geodis_shippingid or gen_id() + return True + + def _geodis_fr_update_tracking(self): + success_pickings = self.env["stock.picking"] + for rec in self: + packages = rec.package_ids + account = rec._geodis_fr_get_auth_tracking() + payload = { + "auth": account, + "tracking": {"shippingId": rec.geodis_shippingid}, + } + ret = roulier.get(rec.delivery_type, "get_tracking_list", payload) + + if len(ret) != 1: + _logger.info("Geodis tracking not found. Picking %s" % rec.id) + continue + # multipack not implemented yet + data = ret[0] + rec.write({"carrier_tracking_ref": data["tracking"]["trackingCode"]}) + packages.write( + { + "parcel_tracking_uri": data["tracking"]["publicUrl"], + "parcel_tracking": data["tracking"]["trackingCode"], + } + ) + success_pickings |= rec + return success_pickings + + def _geodis_fr_get_auth_tracking(self): + """Because it's not the same credentials than + get_label.""" + + account = self._geodis_fr_get_account_tracking() + auth = { + "login": account.account, + "password": account.password, + } + return auth + + def _geodis_fr_get_account_tracking(self): + """Return an 'account'. + + By default, the first account encoutered for this type. + Depending on your case, you may store it on the picking or + compute it from your business rules. + + """ + account = self.env["carrier.account"].search( + [ + ("delivery_type", "=", self.carrier_id.delivery_type), + ("geodis_fr_tracking_account", "=", True), + ], + limit=1, + ) + return account + + def _get_carrier_account_domain(self): + domain = super()._get_carrier_account_domain() + domain.append(("geodis_fr_tracking_account", "!=", True)) + return domain diff --git a/delivery_roulier_geodis_fr/models/stock_quant_package.py b/delivery_roulier_geodis_fr/models/stock_quant_package.py new file mode 100644 index 0000000000..f21146cc21 --- /dev/null +++ b/delivery_roulier_geodis_fr/models/stock_quant_package.py @@ -0,0 +1,48 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +import logging + +from odoo import fields, models + +_logger = logging.getLogger(__name__) + + +class StockQuantPackage(models.Model): + _inherit = "stock.quant.package" + + geodis_cab = fields.Char(help="Barcode of the label") + + def _geodis_fr_parse_response(self, picking, response): + res = self._roulier_parse_response(picking, response) + i = 0 + for rec in self: + rec.write( + { + "geodis_cab": response["parcels"][i]["number"], + "parcel_tracking": picking.geodis_shippingid, + } + ) + i += 1 + # add geodis_shipping_id in res so it is written on picking. + # it is not really a tracking number, it will actually be used to get + # the tracking once the edi file is sent. + # we prefer filling because the parcel tracking ref is used to display the + # label generation button, cancel label button, etc + res["tracking_number"] = ( + not res.get("tracking_number") and picking.geodis_shippingid or "" + ) + return res + + def _geodis_fr_should_include_customs(self, picking): + """Customs documents not implemented.""" + return False + + def _geodis_fr_get_tracking_link(self): + return self.parcel_tracking_uri + + def _get_edi_pack_vals(self): + self.ensure_one() + return { + "barcode": self.geodis_cab, + "weight": self.shipping_weight or self.weight, + } diff --git a/delivery_roulier_geodis_fr/static/src/img/icon.png b/delivery_roulier_geodis_fr/static/src/img/icon.png new file mode 100644 index 0000000000..353b58000e Binary files /dev/null and b/delivery_roulier_geodis_fr/static/src/img/icon.png differ diff --git a/delivery_roulier_geodis_fr/static/src/img/logo.jpg b/delivery_roulier_geodis_fr/static/src/img/logo.jpg new file mode 100644 index 0000000000..2ab9055297 Binary files /dev/null and b/delivery_roulier_geodis_fr/static/src/img/logo.jpg differ diff --git a/delivery_roulier_geodis_fr/tests/__init__.py b/delivery_roulier_geodis_fr/tests/__init__.py new file mode 100644 index 0000000000..5a61787495 --- /dev/null +++ b/delivery_roulier_geodis_fr/tests/__init__.py @@ -0,0 +1 @@ +from . import test_geodis_labels diff --git a/delivery_roulier_geodis_fr/tests/cassettes/GeodisFrLabelCase.test_addresses.yaml b/delivery_roulier_geodis_fr/tests/cassettes/GeodisFrLabelCase.test_addresses.yaml new file mode 100644 index 0000000000..e18838fc95 --- /dev/null +++ b/delivery_roulier_geodis_fr/tests/cassettes/GeodisFrLabelCase.test_addresses.yaml @@ -0,0 +1,105 @@ +interactions: +- request: + body: "\n\n\t\n\t\t\n\t000000\n\tpassword\ + \ \n\n\t\n\t\n\t\t\n FR\n\ + \ 69100\n VILLEURBANNE\n\ + \n\n\t\n" + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '678' + SOAPAction: + - + User-Agent: + - python-requests/2.21.0 + content-type: + - text/xml + method: POST + uri: https://espace-rct.geodis.com/geolabel/services/RechercherLocalite + response: + body: + string: "--MIMEBoundary_a17599f2fbb7c3a49741ffca180b26ffa04943f006c90664\r\n\ + Content-Type: text/xml; charset=UTF-8\r\nContent-Transfer-Encoding: binary\r\ + \nContent-ID: <0.517599f2fbb7c3a49741ffca180b26ffa04943f006c90664@apache.org>\r\ + \n\r\n303506969100VILLEURBANNE\r\ + \n--MIMEBoundary_a17599f2fbb7c3a49741ffca180b26ffa04943f006c90664--\r\n" + headers: + Connection: + - keep-alive + Content-Type: + - multipart/related; boundary="MIMEBoundary_a17599f2fbb7c3a49741ffca180b26ffa04943f006c90664"; + type="text/xml"; start="<0.517599f2fbb7c3a49741ffca180b26ffa04943f006c90664@apache.org>" + Date: + - Tue, 08 Jun 2021 18:44:48 GMT + Set-Cookie: + - NSC_JOaej3fgcb2frlldhm34a0bv1wqbpdp=ffffffff0949c64645525d5f4f58455e445a4a4229f2;expires=Tue, + 08-Jun-2021 18:46:48 GMT;path=/;secure;httponly + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +- request: + body: "\n\n\t\n\t\t\n\t000000\n\tpassword\ + \ \n\n\t\n\t\n\t\t\n FR\n\ + \ 69155\n VILLURB\n\ + \n\n\t\n" + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '673' + SOAPAction: + - + User-Agent: + - python-requests/2.21.0 + content-type: + - text/xml + method: POST + uri: https://espace-rct.geodis.com/geolabel/services/RechercherLocalite + response: + body: + string: "--MIMEBoundary_747599f2fbb7c3a445eb2cca180b26ffc04943f006c90664\r\n\ + Content-Type: text/xml; charset=UTF-8\r\nContent-Transfer-Encoding: binary\r\ + \nContent-ID: <0.647599f2fbb7c3a445eb2cca180b26ffc04943f006c90664@apache.org>\r\ + \n\r\nsoapenv:ServerExceptionGeoLabelMessageT0023Localit\xE9\ + \ inexistante\r\ + \n--MIMEBoundary_747599f2fbb7c3a445eb2cca180b26ffc04943f006c90664--\r\n" + headers: + Connection: + - keep-alive + Content-Length: + - '815' + Content-Type: + - multipart/related; boundary="MIMEBoundary_747599f2fbb7c3a445eb2cca180b26ffc04943f006c90664"; + type="text/xml"; start="<0.647599f2fbb7c3a445eb2cca180b26ffc04943f006c90664@apache.org>" + Date: + - Tue, 08 Jun 2021 18:44:48 GMT + Set-Cookie: + - NSC_JOaej3fgcb2frlldhm34a0bv1wqbpdp=ffffffff0949c64645525d5f4f58455e445a4a4229f2;expires=Tue, + 08-Jun-2021 18:46:48 GMT;path=/;secure;httponly + status: + code: 500 + message: Internal Server Error +version: 1 diff --git a/delivery_roulier_geodis_fr/tests/cassettes/GeodisFrLabelCase.test_labels.yaml b/delivery_roulier_geodis_fr/tests/cassettes/GeodisFrLabelCase.test_labels.yaml new file mode 100644 index 0000000000..bae981cafe --- /dev/null +++ b/delivery_roulier_geodis_fr/tests/cassettes/GeodisFrLabelCase.test_labels.yaml @@ -0,0 +1,95 @@ +interactions: +- request: + body: "\n\n\t\n\t\t\n\t000000\n\tpassword\ + \ \n\n\t\n\t\n\t\t\n 459059\n\ + \ 159\n 000000\n\ + \ Z\n 1\n\ + \ MES\n 00000001\n 20210609\n\ + \ My Company (San Francisco)\n FR\n\ + \ 69\n 69100\n VILLEURBANNE\n\ + \ +1(650)691-3277\n Carrier label test\ + \ customer\n 27 Rue Henri Rolland\n \ + \ FR\n 69100\n VILLEURBANNE\n\ + \ Carrier label test customer\n 1\n\ + \ 1.2\n \n PC\n\ + \ 1.2\n 0\n 1\n\ + \ PACK0000003\n \n\n\ + \n\t\n" + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '1643' + SOAPAction: + - + User-Agent: + - python-requests/2.21.0 + content-type: + - text/xml + method: POST + uri: https://espace-rct.geodis.com/geolabel/services/ImpressionEtiquette + response: + body: + string: "--MIMEBoundary_027599f2fbb7c3a44f903eca180b26ff924943f006c90664\r\n\ + Content-Type: text/xml; charset=UTF-8\r\nContent-Transfer-Encoding: binary\r\ + \nContent-ID: <0.327599f2fbb7c3a44f903eca180b26ff924943f006c90664@apache.org>\r\ + \n\r\ncid:127599f2fbb7c3a44f903eca180b26ff924943f006c90664@apache.orgFR69100VILLEURBANNEM20692LM2069PC1JVGTC1597870000000130PACK0000003\r\ + \n--MIMEBoundary_027599f2fbb7c3a44f903eca180b26ff924943f006c90664\r\nContent-Type:\ + \ application/octet-stream\r\nContent-Transfer-Encoding: binary\r\nContent-ID:\ + \ <127599f2fbb7c3a44f903eca180b26ff924943f006c90664@apache.org>\r\n\r\n^XA\r\ + \n^PR7,7,8^MCY^LRN^FWN^CFD,24^LH10,15^CI0^MNY^MTD^MD0^PON^PMN\r\n^LL1080\r\ + \n^FO0,0^GB830,0,3,B,0^FS\r\n^FO115,0^GB0,120,3,B,0^FS\r\n^FO320,0^GB0,120,3,B,0^FS\r\ + \n^FO576,0^GB0,120,3,B,0^FS\r\n^FO576,60^GB252,0,3,B,0^FS\r\n^FO0,120^GB830,0,3,B,0^FS\r\ + \n^FO216,120^GB0,256,3,B,0^FS\r\n^FO560,120^GB0,56,3,B,0^FS\r\n^FO0,176^GB830,0,3,B,0^FS\r\ + \n^FO0,278^GB216,0,3,B,0^FS\r\n^FO0,376^GB830,0,3,B,0^FS\r\n^FO0,416^GB830,0,1,B,0^FS\r\ + \n^FX --DEBUT IMPRESSION DONNEES-- ^FS\r\n^A0N,135,110^FO5,13^CI0^FDM^FS\r\ + \n^A0N,90,80^FO80,47^CI0^FD2^FS\r\n^A0N,110,120^FO120,30^CI0^FD0^FS\r\n^A0N,140,130^FO180,10^CI0^FD69^FS\r\ + \n^A0N,60,70^FO590,10^CI0^FD^FS\r\n^A0N,60,70^FO590,68^CI0^FD^FS\r\n^A0N,25,18^FO0,128^CI0^FD159\ + \ - FE LILLE^FS\r\n^A0N,25,18^FO0,152^CI0^FDTel: 0892052828^FS\r\n^A0N,25,18^FO224,152^CI0^FDShp:^FS\r\ + \n^A0N,45,30^FO260,136^CI0^FD00000001^FS\r\n^A0N,25,18^FO376,152^CI0^FDfrom^FS\r\ + \n^A0N,45,30^FO416,136^CI0^FD09/06/2021^FS\r\n^A0N,45,30^FO568,136^CI0^FD^FS\r\ + \n^A0N,38,20^FO0,180^CI0^FDMY COMPANY SAN FRANC^FS\r\n^A0N,38,20^FO0,214^CI0^FDFR\ + \ 69 VILLEURBANNE^FS\r\n^A0N,38,20^FO0,248^CI0^FD1 650 691-3277^FS\r\n^A0N,45,35^FO228,184^CI0^FDCARRIER\ + \ LABEL TEST CUSTOMER^FS\r\n^A0N,30,25^FO228,224^CI0^FD27 RUE HENRI ROLLAND^FS\r\ + \n^A0N,30,25^FO228,254^CI0^FD^FS\r\n^A0N,45,35^FO228,284^CI0^FDFR 69100 VILLEURBANNE^FS\r\ + \n^A0N,45,35^FO228,336^CI0^FDCARRIER LABEL TEST CUSTOMER^FS\r\n^A0N,25,18^FO0,288^CI0^FDUM^FS\r\ + \n^A0N,25,18^FO0,320^CI0^FDWGHT^FS\r\n^A0N,25,18^FO0,352^CI0^FDVOL^FS\r\n\ + ^A0N,37,25^FO80,284^CI0^FD1/1^FS\r\n^A0N,37,25^FO80,316^CI0^FD1.2/1.2^FS\r\ + \n^A0N,37,25^FO80,348^CI0^FD-/-^FS\r\n^A0N,25,18^FO0,388^CI0^FDRef2 clt :^FS\r\ + \n^A0N,30,25^FO90,384^CI0^FD^FS\r\n^A0N,25,18^FO0,428^CI0^FDRef colis :^FS\r\ + \n^A0N,30,25^FO90,424^CI0^FDPACK0000003^FS\r\n^FO0,456^GB830,0,1,B,0^FS\r\n\ + ^BY3^FO49,732^BCN,180,N,N,N,A^FN1^FS\r\n^A0N,24,24^FO138,922^CI0^FN2^FS\r\n\ + ^FN1^FDJVGTC1597870000000130^FS\r\n^FN2^FDJVGTC1597870000000130^FS\r\n^BY3^FO280,470^BCN,180,N,N,N,A^FN3^FS\r\ + \n^A0N,24,24^FO420,655^CI0^FN4^FS\r\n^FN3^FD2LM2069^FS\r\n^FN4^FD2LM2069^FS\r\ + \n^A0N,35,35^FO640,980^CI0^FD^FS\r\n^FO0,1000^GB840,0,2,B,0^FS\r\n^FO0,1037^GB840,0,2,B,0^FS\r\ + \n^A0N,32,28^FO5,1006^CI0^FD^FS\r\n^FX --FIN IMPRESSION DONNEES-- ^FS\r\n\ + ^PQ1,0,1,Y\r\n^XZ\r\n\r\n--MIMEBoundary_027599f2fbb7c3a44f903eca180b26ff924943f006c90664--\r\ + \n" + headers: + Connection: + - keep-alive + Content-Type: + - multipart/related; boundary="MIMEBoundary_027599f2fbb7c3a44f903eca180b26ff924943f006c90664"; + type="text/xml"; start="<0.327599f2fbb7c3a44f903eca180b26ff924943f006c90664@apache.org>" + Date: + - Tue, 08 Jun 2021 17:07:18 GMT + Set-Cookie: + - NSC_JOaej3fgcb2frlldhm34a0bv1wqbpdp=ffffffff0949c64645525d5f4f58455e445a4a4229f2;expires=Tue, + 08-Jun-2021 17:09:18 GMT;path=/;secure;httponly + Transfer-Encoding: + - chunked + status: + code: 200 + message: OK +version: 1 diff --git a/delivery_roulier_geodis_fr/tests/test_geodis_labels.py b/delivery_roulier_geodis_fr/tests/test_geodis_labels.py new file mode 100644 index 0000000000..ef818dd5cd --- /dev/null +++ b/delivery_roulier_geodis_fr/tests/test_geodis_labels.py @@ -0,0 +1,123 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from vcr_unittest import VCRMixin + +from odoo.addons.base_delivery_carrier_label.tests import carrier_label_case + + +class GeodisFrLabelCase(VCRMixin, carrier_label_case.TestCarrierLabel): + def setUp(self, *args, **kwargs): + # need it to be defined before super to avoid failure in _hide_sensitive_data + self.account = False + super().setUp(*args, **kwargs) + # french carrier sender need to be from France + self.picking.company_id.partner_id.write( + { + "country_id": self.env.ref("base.fr").id, + "city": "VILLEURBANNE", + "zip": "69100", + } + ) + self.account = self.env["carrier.account"].create( + { + "name": "Geodis - France Express", + "delivery_type": "geodis_fr", + # fill real account information if you want to re-generate cassette + "account": "45393e38323033372b3c3334", + "password": "2d5a44584356", + "geodis_fr_customer_id": "787000", + } + ) + carrier = self.env.ref("delivery_roulier_geodis_fr.delivery_carrier_mes") + carrier.carrier_account_id = self.account.id + self.agency = self.env["delivery.carrier.agency"].create( + { + "name": "Lille Agency", + "external_reference": "459059", + "delivery_type": "geodis_fr", + "partner_id": self.env.ref( + "delivery_roulier_geodis_fr.default_geodis_france_exress_agency_partner" + ).id, + "geodis_fr_interchange_sender": "222222222", + "geodis_fr_interchange_recipient": "111111111", + "geodis_fr_hub_id": "159", + } + ) + + def _hide_sensitive_data(self, request): + password = self.account and self.account.password or "dummy" + account = self.account and self.account.account or "dummy" + customer_id = self.account and self.account.geodis_fr_customer_id or "dummy" + body = request.body + body = body.replace(password.encode(), b"password") + body = body.replace(account.encode(), b"000000") + body = body.replace(customer_id.encode(), b"000000") + request.body = body + return request + + def _get_vcr_kwargs(self, **kwargs): + return { + "record_mode": "once", + "match_on": ["method", "path"], + "decode_compressed_response": True, + "before_record_request": self._hide_sensitive_data, + } + + def _transfer_order_picking(self): + for move in self.picking.move_ids: + move.quantity_done = move.product_uom_qty + move_lines = self.picking.move_line_ids + self.picking._put_in_pack(move_lines) + return super()._transfer_order_picking() + + def _product_data(self): + data = super()._product_data() + data.update( + { + "weight": 1.2, + } + ) + return data + + def _create_order_picking(self): + return super()._create_order_picking() + + def _get_carrier(self): + return self.env.ref("delivery_roulier_geodis_fr.delivery_carrier_mes") + + def _partner_data(self): + data = super()._partner_data() + data.update( + { + "street": "27 Rue Henri Rolland", + "zip": "69100", + "city": "VILLEURBANNE", + "country_id": self.env.ref("base.fr").id, + } + ) + return data + + def test_labels_and_edi(self): + res = super().test_labels() + self.assertTrue(self.picking.geodis_shippingid) + deposit = self.env["deposit.slip"].create( + { + "name": "test", + "delivery_type": "geodis_fr", + "picking_ids": [(6, 0, self.picking.ids)], + } + ) + deposit.validate_deposit() + attachment = self.env["ir.attachment"].search( + [("res_id", "=", deposit.id), ("res_model", "=", "deposit.slip")] + ) + self.assertEqual(len(attachment), 1) + self.assertTrue(attachment.datas) + return res + + def test_addresses(self): + addresses = self.picking._geodis_fr_get_address_proposition() + self.assertEqual(len(addresses), 1) + self.picking.partner_id.write({"zip": 69155, "city": "VILLURB"}) + addresses = self.picking._geodis_fr_get_address_proposition(raise_address=False) + self.assertEqual(len(addresses), 0) diff --git a/delivery_roulier_geodis_fr/views/carrier_account_views.xml b/delivery_roulier_geodis_fr/views/carrier_account_views.xml new file mode 100644 index 0000000000..50a5c55398 --- /dev/null +++ b/delivery_roulier_geodis_fr/views/carrier_account_views.xml @@ -0,0 +1,28 @@ + + + + + carrier.account + + + + + + + + + + + diff --git a/delivery_roulier_geodis_fr/views/delivery_carrier_agency_views.xml b/delivery_roulier_geodis_fr/views/delivery_carrier_agency_views.xml new file mode 100644 index 0000000000..3050927ef9 --- /dev/null +++ b/delivery_roulier_geodis_fr/views/delivery_carrier_agency_views.xml @@ -0,0 +1,28 @@ + + + + + delivery.carrier.agency + + + + + + + + + + + diff --git a/setup/delivery_roulier_geodis_fr/odoo/addons/delivery_roulier_geodis_fr b/setup/delivery_roulier_geodis_fr/odoo/addons/delivery_roulier_geodis_fr new file mode 120000 index 0000000000..2a2e878ffc --- /dev/null +++ b/setup/delivery_roulier_geodis_fr/odoo/addons/delivery_roulier_geodis_fr @@ -0,0 +1 @@ +../../../../delivery_roulier_geodis_fr \ No newline at end of file diff --git a/setup/delivery_roulier_geodis_fr/setup.py b/setup/delivery_roulier_geodis_fr/setup.py new file mode 100644 index 0000000000..28c57bb640 --- /dev/null +++ b/setup/delivery_roulier_geodis_fr/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)