first commit

This commit is contained in:
root
2026-03-14 09:42:12 +00:00
commit 0adbd20c2c
10991 changed files with 1646955 additions and 0 deletions

View File

@@ -0,0 +1,6 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from . import routes
__all__ = [routes]

View File

@@ -0,0 +1,23 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import PoolMeta
class Carrier(metaclass=PoolMeta):
__name__ = 'carrier'
@property
def vsf_carrier_title(self):
return self.party.name
@property
def vsf_method_title(self):
return self.carrier_product.name
def get_vsf(self):
return {
'carrier_code': str(self.id),
'method_code': str(self.id),
'carrier_title': self.vsf_carrier_title,
'method_title': self.vsf_method_title,
}

View File

@@ -0,0 +1,16 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.protocols.wrappers import exceptions
class LoginException(exceptions.Unauthorized):
pass
class NotFound(exceptions.NotFound):
pass
class BadRequest(exceptions.BadRequest):
pass

View File

@@ -0,0 +1,14 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.pool import PoolMeta
class Cron(metaclass=PoolMeta):
__name__ = 'ir.cron'
@classmethod
def __setup__(cls):
super().__setup__()
cls.method.selection.extend([
('web.shop|vsf_update', "Update Vue Storefront"),
])

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,83 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Telèfon"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Index Elasticsearch"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "URL Elasticsearch"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Registre"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Gran total"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr "El indentificador Vue Storefront ha de ser únic per a cada registre."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "El númbero \"%(code)s\" no és un identificador fiscal vàlid."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"No us heu identificat correctament o el vostre compte s'ha suspes "
"temporalment."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Enviament"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Subtotal"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Impost"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "El codi de país \"%(code)s\" és desconegut."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Identificador Vue Storefront"
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Identificador botiga web Vue Storefront"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Actualitza Vue Storefront"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,84 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Telefon"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Elasticsearch Index"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "Elasticsearch URL"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Datensatz"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Gesamtbetrag"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
"Der Vue Storefront Identifikator muss für jeden Datensatz eindeutig sein."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "Die Nummer \"%(code)s\" ist kein gültiger Steueridentifikator."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"Sie haben sich nicht korrekt angemeldet oder Ihr Konto ist vorübergehend "
"deaktiviert."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Versand"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Zwischensumme"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Steuer"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "Der Ländercode \"%(code)s\" ist unbekannt."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Vue Storefront Identifikator"
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Webshop Vue Storefront Identifikator"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Vue Storefront aktualisieren"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,83 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Teléfono"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Índice Elasticsearch"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "URL Elasticsearch"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Registro"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Gran Total"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr "El identificador Vue Storefront debe ser único por cada registro."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "El número \"%(code)s\" no es un identificador fiscal válido."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"No te has identificado correctamente o tu cuenta ha sido temporalmente "
"suspendida."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Envío"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Subtotal"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Impuesto"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "El código de país \"%(code)s\" es desconocido."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Identificador Vue Storefront"
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Identificador de la tienda Vue Storefront"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Actualizar Vue Storefront"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,84 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Téléphone"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "UGS"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "UGS"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Index Elasticsearch"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "URL d'Elasticsearch"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Enregistrement"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Somme finale"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
"L'identifiant Vue Storefront doit être unique pour chaque enregistrement."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "Le numéro « %(code)s » n'est pas un identifiant fiscal valide."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"Vous ne vous êtes pas connecté correctement ou votre compte est "
"temporairement désactivé."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Livraison"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Sous-total"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Taxe"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "Le code pays « %(code)s » n'est pas connu."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Identifiant Vue Storefront"
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Identifiant de la boutique Web Vsf"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Mettre à jour Vue Storefront"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Telepon"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Pajak"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "Kode negara \"%(code)s\" tidak diketahui."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,85 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Telefono"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Indice Elasticsearch"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "URL Elasticsearch"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Record"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Somma totale"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
"L'identificatore di Vue Storefront deve essere univoco per ogni record."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "Il numero \"%(code)s\" non è un codice fiscale valido."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"Non hai eseguito l'accesso correttamente o il tuo account è temporaneamente "
"disabilitato."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Spedizione"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Subtotale"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Imposta"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "Il codice paese \"%(code)s\" è sconoscito."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Identificatore Vue Storefront"
#, fuzzy
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Identificatore negozio web Vue Storefront"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Aggiorna Vue Storefront"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,83 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Telefoon"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Elasticsearch-index"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "Elasticsearch-URL"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Record"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Eindtotaal"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr "De Vue Storefront-ID moet uniek zijn voor elk record."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "Het nummer \"%(code)s\" is geen geldig belastingnummer."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"U heeft zich niet correct aangemeld of uw account is tijdelijk "
"uitgeschakeld."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Verzenden"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Subtotaal"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Belasting"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "De landcode \"%(code)s\" is niet gekend."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Vue Storefront-identificatie"
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Webwinkel VSF identificatie"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Update Vue Storefront"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,85 @@
#
#, fuzzy
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr "Telefone"
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr "SKU"
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr "Índice Elasticsearch"
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr "URL do Elasticsearch"
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr "Registro"
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr "Total Geral"
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
"O identificador do Vue Storefront Deve ser Exclusivo Para Cada Registro."
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr "O Número \"%(code)s\" não é um Identificador Fiscal Válido."
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
"Você não fez Login Corretamente ou sua Conta está Temporariamente "
"Desativada."
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr "Envio"
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr "Subtotal"
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr "Imposto"
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr "O Código do País \"%(code)s\" não é Conhecido."
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr "Identificador Vue Storefront"
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr "Identificador VSF da Loja Virtual"
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr "Atualizar Vue Storefront"
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr "Vue Storefront"
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr "Vue Storefront"

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,81 @@
#
msgid ""
msgstr "Content-Type: text/plain; charset=utf-8\n"
msgctxt "field:party.address,vsf_telephone:"
msgid "Telephone"
msgstr ""
msgctxt "field:product.product,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:product.template,vsf_sku:"
msgid "SKU"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_index:"
msgid "Elasticsearch Index"
msgstr ""
msgctxt "field:web.shop,vsf_elasticsearch_url:"
msgid "Elasticsearch URL"
msgstr ""
msgctxt "field:web.shop.vsf_identifier,record:"
msgid "Record"
msgstr ""
msgctxt "model:ir.message,text:msg_grand_total"
msgid "Grand Total"
msgstr ""
msgctxt "model:ir.message,text:msg_identifier_record_unique"
msgid "The Vue Storefront identifier must be unique for each record."
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_invalid_tax_identifier"
msgid "The number \"%(code)s\" is not a valid tax identifier."
msgstr ""
msgctxt "model:ir.message,text:msg_login_wrong"
msgid "You did not sign in correctly or your account is temporarily disabled."
msgstr ""
msgctxt "model:ir.message,text:msg_shipping"
msgid "Shipping"
msgstr ""
msgctxt "model:ir.message,text:msg_subtotal"
msgid "Subtotal"
msgstr ""
msgctxt "model:ir.message,text:msg_tax"
msgid "Tax"
msgstr ""
#, python-format
msgctxt "model:ir.message,text:msg_unknown_country_code"
msgid "The country code \"%(code)s\" is not known."
msgstr ""
msgctxt "model:ir.message,text:msg_vsf_identifier"
msgid "Vue Storefront Identifier"
msgstr ""
msgctxt "model:web.shop.vsf_identifier,string:"
msgid "Web Shop Vsf Identifier"
msgstr ""
msgctxt "selection:ir.cron,method:"
msgid "Update Vue Storefront"
msgstr ""
msgctxt "selection:web.shop,type:"
msgid "Vue Storefront"
msgstr ""
msgctxt "view:web.shop:"
msgid "Vue Storefront"
msgstr ""

View File

@@ -0,0 +1,34 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data grouped="1">
<record model="ir.message" id="msg_identifier_record_unique">
<field name="text">The Vue Storefront identifier must be unique for each record.</field>
</record>
<record model="ir.message" id="msg_vsf_identifier">
<field name="text">Vue Storefront Identifier</field>
</record>
<record model="ir.message" id="msg_login_wrong">
<field name="text">You did not sign in correctly or your account is temporarily disabled.</field>
</record>
<record model="ir.message" id="msg_invalid_tax_identifier">
<field name="text">The number "%(code)s" is not a valid tax identifier.</field>
</record>
<record model="ir.message" id="msg_unknown_country_code">
<field name="text">The country code "%(code)s" is not known.</field>
</record>
<record model="ir.message" id="msg_subtotal">
<field name="text">Subtotal</field>
</record>
<record model="ir.message" id="msg_tax">
<field name="text">Tax</field>
</record>
<record model="ir.message" id="msg_grand_total">
<field name="text">Grand Total</field>
</record>
<record model="ir.message" id="msg_shipping">
<field name="text">Shipping</field>
</record>
</data>
</tryton>

View File

@@ -0,0 +1,135 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from stdnum import get_cc_module
try:
import phonenumbers
from phonenumbers import NumberParseException, PhoneNumberFormat
except ImportError:
phonenumbers = None
from trytond.i18n import gettext
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from trytond.tools import remove_forbidden_chars
from .exceptions import BadRequest
from .web import join_name, split_name
class Address(metaclass=PoolMeta):
__name__ = 'party.address'
vsf_telephone = fields.Many2One(
'party.contact_mechanism', "Telephone",
domain=[
('party', '=', Eval('party', -1)),
('type', 'in', ['phone', 'mobile']),
])
def get_vsf(self, for_party=None):
if for_party and for_party != self.party:
firstname, lastname = split_name(
self.party_name or for_party.name)
else:
firstname, lastname = split_name(
self.party_name or self.party.name)
address = {
'id': self.id,
'firstname': firstname,
'lastname': lastname,
'street': self.street.splitlines(),
'city': self.city,
'country_id': self.country.code if self.country else None,
'postcode': self.postal_code,
}
if self.subdivision:
address['region'] = {
'region': self.subdivision.name,
}
if self.vsf_telephone:
address['telephone'] = self.vsf_telephone.value
if for_party:
address['company'] = self.party.name
address['vat_id'] = (
self.party.tax_identifier.code
if self.party.tax_identifier else None)
return address
def set_vsf(self, data, for_party=None):
pool = Pool()
Country = pool.get('country.country')
Subdivision = pool.get('country.subdivision')
ContactMechanism = pool.get('party.contact_mechanism')
name = remove_forbidden_chars(
join_name(data['firstname'], data['lastname']))
party = for_party or self.party
if name != party.name:
self.party_name = name
self.street = '\n'.join(map(str, data['street']))
self.city = remove_forbidden_chars(data['city'])
if data['country_id']:
try:
self.country, = Country.search([
('code', '=', data['country_id']),
], limit=1)
except ValueError:
raise BadRequest(gettext(
'web_shop_vue_storefront.msg_unknown_country_code',
code=data['country_id']))
self.postal_code = data['postcode']
if data.get('region') and data['region']['region']:
domain = [
('name', '=', data['region']['region']),
]
if self.country:
domain.append(('country', '=', self.country.id))
subdivisions = Subdivision.search(domain, limit=1)
if subdivisions:
self.subdivision, = subdivisions
if data.get('telephone'):
value = remove_forbidden_chars(data['telephone'])
if phonenumbers:
try:
phonenumber = phonenumbers.parse(
data['telephone'], data['country_id'])
value = phonenumbers.format_number(
phonenumber, PhoneNumberFormat.INTERNATIONAL)
except NumberParseException:
pass
contacts = ContactMechanism.search([
('party', '=', self.party.id),
('type', 'in', ['phone', 'mobile']),
('value', '=', value),
], limit=1)
if contacts:
self.vsf_telephone, = contacts
else:
contact = ContactMechanism(
party=self.party,
type='phone',
value=value)
contact.save()
self.vsf_telephone = contact
class Identifier(metaclass=PoolMeta):
__name__ = 'party.identifier'
def set_vsf_tax_identifier(self, code):
pool = Pool()
Party = pool.get('party.party')
for type in Party.tax_identifier_types():
module = get_cc_module(*type.split('_', 1))
if module and module.is_valid(code):
self.type = type
self.code = code
break
else:
raise BadRequest(gettext(
'web_shop_vue_storefront.msg_invalid_tax_identifier',
code=code))

View File

@@ -0,0 +1,258 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import json
from operator import attrgetter
from trytond.model import fields
from trytond.pool import Pool, PoolMeta
from trytond.tools import slugify
from .web import ShopVSFIdentifierMixin
class _ProductEntityMixin:
__slots__ = ()
vsf_sku = fields.Function(
fields.Char("SKU"), 'get_vsf_sku', searcher='search_vsf_sku')
def get_vsf_sku(self, name):
return self.code
@classmethod
def search_vsf_sku(cls, name, clause):
return [('code',) + tuple(clause[1:])]
@property
def vsf_image(self):
try:
return self.get_image_url(_external=True, web_shop=1)
except AttributeError:
pass
@property
def vsf_type_id(self):
return 'virtual' if self.type == 'service' else 'simple'
@property
def vsf_quantity(self):
pool = Pool()
Uom = pool.get('product.uom')
quantity = self.forecast_quantity
if quantity < 0:
quantity = 0
return Uom.compute_qty(self.default_uom, quantity, self.sale_uom)
def get_vsf_entity(self, shop, price, tax):
categories = [
c for c in self.categories_all
if c in shop.categories]
status = 1
if not self.salable:
status = 2
quantity = self.vsf_quantity
if quantity <= 0:
status = 3
return {
'id': self.vsf_identifier.id,
'name': self.name,
'image': self.vsf_image,
'sku': self.vsf_sku,
'url_key': slugify(self.name.lower()),
'url_path': '%s/%s' % (
self.vsf_identifier.id, slugify(self.name.lower())),
'type_id': self.vsf_type_id,
'price': float(price) if price is not None else None,
'price_tax': float(tax) if tax is not None else None,
'price_incl_tax': (
float(price + tax)
if price is not None and tax is not None else None),
'status': status,
'visibility': 4,
'category_ids': [c.vsf_identifier.id for c in categories],
'category': [c.get_vsf_entity_product(shop) for c in categories],
'stock': [{
'is_in_stock': quantity > 0,
'qty': quantity,
}],
}
def get_vsf_stock(self):
quantity = self.vsf_quantity
return {
'product_id': self.vsf_identifier.id,
'qty': quantity,
'is_in_stock': quantity > 0,
}
class Product(_ProductEntityMixin, ShopVSFIdentifierMixin, metaclass=PoolMeta):
__name__ = 'product.product'
def vsf_is_configurable(self, shop):
return False
class Template(
_ProductEntityMixin, ShopVSFIdentifierMixin, metaclass=PoolMeta):
__name__ = 'product.template'
@property
def vsf_type_id(self):
return 'configurable'
def get_vsf_products(self, shop):
return [p for p in self.products if shop in p.web_shops]
class Category(ShopVSFIdentifierMixin, metaclass=PoolMeta):
__name__ = 'product.category'
def get_vsf_entity(self, shop):
pool = Pool()
Product = pool.get('product.product')
count = Product.search([
('categories_all', '=', self.id),
('web_shops', '=', shop.id),
], count=True)
paths = self.get_vsf_paths(shop)
def children(category):
return [{
'id': c.vsf_identifier.id,
'children_data': children(c),
} for c in category.childs
if c in shop.categories]
return {
'id': self.vsf_identifier.id,
'name': self.name,
'parent_id': (self.parent.vsf_identifier.id
if self.parent in shop.categories else None),
'path': '/'.join([str(p.vsf_identifier.id) for p in paths]),
'url_key': slugify(self.name.lower()),
'url_path': '/'.join(
map(slugify, map(str.lower, map(attrgetter('name'), paths)))),
'is_active': True,
'position': self.id,
'level': len(paths),
'product_count': count,
'children_data': children(self),
}
def get_vsf_entity_product(self, shop):
return {
'category_id': self.vsf_identifier.id,
'name': self.name,
'slug': slugify(self.name),
'path': '/'.join(
map(slugify, map(attrgetter('name'),
self.get_vsf_paths(shop)))),
}
def get_vsf_paths(self, shop):
category = self
paths = [category]
while category.parent:
if category.parent not in shop.categories:
break
category = category.parent
paths.append(category)
return list(reversed(paths))
class ProductAttribute(metaclass=PoolMeta):
__name__ = 'product.product'
def get_vsf_entity(self, shop, price, tax):
entity = super().get_vsf_entity(shop, price, tax)
if self.attribute_set:
for attribute in self.attribute_set.attributes:
if attribute not in shop.attributes:
continue
name = attribute.name
value = self.attributes.get(name)
options = {
o['name']: o['value']
for o in attribute.get_vsf_options()}
entity[name] = options.get(value, value)
return entity
def vsf_is_configurable(self, shop):
configurable = super().vsf_is_configurable(shop)
if self.template.attribute_set:
template_products = self.template.get_vsf_products(shop)
if len(template_products) > 1:
names = [
a.name for a in self.template.attribute_set.attributes
if a in shop.attributes]
values = {filter(
lambda k: k in names, sorted(p.attributes.keys()))
for p in template_products}
configurable = len(values) == len(template_products)
return configurable
class TemplateAttribute(metaclass=PoolMeta):
__name__ = 'product.template'
def get_vsf_entity(self, shop, price, tax):
entity = super().get_vsf_entity(shop, price, tax)
if self.attribute_set:
for attribute in self.attribute_set.attributes:
if attribute not in shop.attributes:
continue
name = attribute.name
p_attr_values = {
p.attributes[name]
for p in self.products
if name in p.attributes}
if attribute.type_ == 'multiselection':
p_attr_values = set(sum(p_attr_values, []))
options = [
o['value'] for o in attribute.get_vsf_options()
if o['name'] in p_attr_values]
entity[name + '_options'] = options
config_options = entity.setdefault('configurable_options', [])
config_options.append(
attribute.get_vsf_entity_template(self, p_attr_values))
return entity
class Attribute(ShopVSFIdentifierMixin, metaclass=PoolMeta):
__name__ = 'product.attribute'
def get_vsf_options(self):
return [{
'value': i,
'name': n,
'label': s,
} for i, (n, s) in enumerate(
json.loads(self.selection_json), 1)]
def get_vsf_entity(self, shop):
return {
'id': self.vsf_identifier.id,
'attribute_id': self.vsf_identifier.id,
'attribute_code': self.name,
'frontend_input': self.type_,
'frontend_label': self.string,
'is_comparable': True,
'is_user_defined': True,
'is_visible': True,
'is_visible_on_front': True,
'options': self.get_vsf_options(),
}
def get_vsf_entity_template(self, template, attribute_values):
values = [{
'value_index': o['value'],
'label': o['label'],
} for o in self.get_vsf_options()
if o['name'] in attribute_values]
return {
'attribute_id': self.vsf_identifier.id,
'attribute_code': self.name,
'label': self.string,
'product_id': template.vsf_identifier.id,
'values': values,
}

View File

@@ -0,0 +1,53 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
import logging
from trytond import backend
from trytond.exceptions import UserError, UserWarning
from trytond.protocols.wrappers import exceptions, with_pool, with_transaction
from trytond.transaction import Transaction
from trytond.wsgi import app
logger = logging.getLogger(__name__)
@app.route(
'/<database_name>/web_shop_vue_storefront/<shop>/<target>/<action>',
methods={'POST', 'GET'})
@app.route(
'/<database_name>/web_shop_vue_storefront/<shop>/<target>/<action>/<sku>',
methods={'GET'})
@with_pool
@with_transaction(context={'_skip_warnings': True})
def route(request, pool, shop, target, action, sku=None):
Shop = pool.get('web.shop')
Session = pool.get('web.user.session')
shop = Shop.get(shop)
method = '_'.join(
[request.method, 'vsf', target, action.replace('-', '_')])
if 'token' in request.args:
with Transaction().new_transaction():
try:
Session.reset(request.args['token'])
except backend.DatabaseOperationalError:
logger.debug('Reset session failed', exc_info=True)
try:
if request.data:
data = request.parsed_data
else:
data = None
kwargs = request.args.to_dict()
if sku is not None:
kwargs['sku'] = sku
with Transaction().set_context(**shop.get_context()):
result = getattr(shop, method)(data, **kwargs)
except exceptions.HTTPException as exception:
Transaction().rollback()
return {'code': exception.code, 'result': exception.description}
except (UserError, UserWarning) as exception:
Transaction().rollback()
return {'code': '400', 'result': str(exception)}
except Exception as exception:
Transaction().rollback()
return {'code': 500, 'result': str(exception)}
return {'code': 200, 'result': result}

View File

@@ -0,0 +1,249 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from decimal import Decimal
from trytond.i18n import gettext
from trytond.pool import Pool, PoolMeta
from .exceptions import BadRequest
from .web import split_name
class Sale(metaclass=PoolMeta):
__name__ = 'sale.sale'
@property
def vsf_id(self):
if self.web_shop:
if self.web_shop.guest_party == self.party:
return self.web_id
else:
return self.id
def get_vsf_user_order_history(self):
firstname, lastname = split_name(self.party.name)
return {
'entity_id': self.id,
'increment_id': self.number,
'created_at': self.create_date.isoformat(),
'customer_firstname': firstname,
'customer_lastname': lastname,
'grand_total': float(self.total_amount),
'status': self.state_string,
'items': [
line.get_vsf_user_order_history() for line in self.lines],
}
@classmethod
def search_vsf(cls, cart_id, shop, user=None):
domain = [
('web_shop', '=', shop.id),
('state', '=', 'draft'),
]
# cart_id may be either id and web_id
cart_id_domain = ['OR']
domain.append(cart_id_domain)
try:
sale_id = int(cart_id)
except ValueError:
pass
else:
if user:
party = user.party
cart_id_domain.append([
('id', '=', sale_id),
('party', '=', party.id),
])
cart_id_domain.append(('web_id', '=', cart_id))
try:
sale, = cls.search(domain, limit=1)
except ValueError:
raise BadRequest()
return sale
@property
def vsf_subtotal(self):
return self.untaxed_amount
def set_vsf(self, data, user=None):
if user and self.web_shop and self.party == self.web_shop.guest_party:
self.party = user.party
self.on_change_party()
if user and 'addressInformation' in data:
addresses = data['addressInformation']
address_data = addresses.get('shippingAddress')
if address_data:
address = user.set_vsf_address(address_data, self.party)
address.save()
if address.party != self.party:
self.shipment_party = address.party
self.shipment_address = address
address_data = addresses.get('billingAddress')
if address_data:
if address_data != addresses.get('shippingAddress'):
address = user.set_vsf_address(address_data, self.party)
address.save()
if address.party != self.party:
self.invoice_party = address.party
self.invoice_address = address
def get_vsf(self):
return {
'grand_total': float(self.total_amount),
'items': [line.get_vsf() for line in self.lines if line.product],
'total_segments': [{
'code': 'subtotal',
'title': gettext('web_shop_vue_storefront.msg_subtotal'),
'value': float(self.vsf_subtotal),
}, {
'code': 'tax',
'title': gettext('web_shop_vue_storefront.msg_tax'),
'value': float(self.tax_amount),
}, {
'code': 'grand_total',
'title': gettext(
'web_shop_vue_storefront.msg_grand_total'),
'value': float(self.total_amount),
}],
}
class SaleShipmentCost(metaclass=PoolMeta):
__name__ = 'sale.sale'
def get_vsf(self):
pool = Pool()
Tax = pool.get('account.tax')
item = super().get_vsf()
if self.carrier:
cost = self.compute_shipment_cost(self.carrier)
if cost is not None:
cost_line = self.get_shipment_cost_line(self.carrier, cost)
taxes = Tax.compute(cost_line.taxes, cost, 1, self.tax_date)
cost += sum(t['amount'] for t in taxes)
cost = float(self.currency.round(cost))
item['grand_total'] += cost
item['total_segments'].insert(1, {
'code': 'shipping',
'title': gettext(
'web_shop_vue_storefront.msg_shipping'),
'value': cost,
})
for segment in item['total_segments']:
if segment['code'] == 'grand_total':
segment['value'] += cost
return item
def set_vsf_shipping_methods(self, data):
pool = Pool()
Country = pool.get('country.country')
Address = pool.get('party.address')
try:
country, = Country.search([
('code', '=', data['address']['country_id']),
])
except ValueError:
raise BadRequest()
if not self.shipment_address:
self.shipment_address = Address()
self.shipment_address.country = country
def set_vsf(self, data, user):
pool = Pool()
Carrier = pool.get('carrier')
super().set_vsf(data, user)
if 'addressInformation' in data:
if data['addressInformation']['shipping_carrier_code']:
try:
carrier_id = int(
data['addressInformation']['shipping_carrier_code'])
except ValueError:
raise BadRequest()
try:
carrier, = Carrier.search([
('id', '=', carrier_id),
], limit=1)
except ValueError:
raise BadRequest()
else:
carrier = None
# Use id to get proper context
self.carrier = carrier.id if carrier else None
if not self.carrier:
self.shipment_cost_method = None
class Line(metaclass=PoolMeta):
__name__ = 'sale.line'
@property
def vsf_tax_amount(self):
return sum((t.amount for t in self._get_taxes().values()), Decimal(0))
def get_vsf_user_order_history(self):
amount_incl_tax = self.amount + self.vsf_tax_amount
digits = self.__class__.unit_price.digits
price_incl_tax = (
amount_incl_tax / Decimal(str(self.quantity))
).quantize(Decimal(1) / 10 ** digits[1])
return {
'name': self.product.name if self.product else '',
'sku': self.product.vsf_sku if self.product else '',
'price_incl_tax': float(price_incl_tax),
'qty_ordered': self.quantity,
'row_total_incl_tax': float(amount_incl_tax),
}
def get_vsf(self):
assert self.product
amount_incl_tax = self.amount + self.vsf_tax_amount
digits = self.__class__.unit_price.digits
price_incl_tax = (
amount_incl_tax / Decimal(str(self.quantity))
).quantize(Decimal(1) / 10 ** digits[1])
return {
'item_id': self.id,
'sku': self.product.vsf_sku,
'qty': self.quantity,
'name': self.product.name,
'price': float(price_incl_tax),
'product_type': self.product.vsf_type_id,
'quote_id': self.sale.id,
'product_option': {
'extension_attributes': {
},
},
}
def set_vsf(self, data):
pool = Pool()
Product = pool.get('product.product')
try:
self.product, = Product.search([
('vsf_sku', '=', data['sku']),
], limit=1)
except ValueError:
raise BadRequest()
self.quantity = data['qty']
self.on_change_product()
class LineAttribute(metaclass=PoolMeta):
__name__ = 'sale.line'
def get_vsf_cart(self):
assert self.sale.web_shop
item = super().get_vsf_cart()
if self.product.attributes_set:
product_option = item['product_option']
extension_attributes = product_option['extension_attributes']
extension_attributes['configurable_item_options'] = options = []
for attribute in self.product.attributes_set.attributes:
if attribute not in self.sale.web_shop.attributes:
continue
options.append({
'option_id': attribute.vsf_identifier.id,
'option_value': self.attributes.get(attribute.name),
})
return item

View File

@@ -0,0 +1,2 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.

View File

@@ -0,0 +1,278 @@
================================
Web Shop Vue Storefront Scenario
================================
Imports::
>>> import base64
>>> from decimal import Decimal
>>> from unittest.mock import ANY, MagicMock, patch
>>> from proteus import Model
>>> from trytond.modules.account.tests.tools import create_chart, get_accounts
>>> from trytond.modules.company.tests.tools import create_company
>>> from trytond.modules.web_shop_vue_storefront import web
>>> from trytond.modules.web_shop_vue_storefront.tests.tools import AnyDictWith
>>> from trytond.tests.tools import activate_modules
>>> pixel = base64.decodebytes(
... b'/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAUDBAQEAwUEBAQFBQUGBwwIBwcHBw8LCwkMEQ8S'
... b'EhEPERETFhwXExQaFRERGCEYGh0dHx8fExciJCIeJBweHx7/wAALCAABAAEBAREA/8QAHwAA'
... b'AQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQR'
... b'BRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RF'
... b'RkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ip'
... b'qrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEB'
... b'AAA/APsuv//Z')
Patch elasticsearch::
>>> es = MagicMock()
>>> _ = patch.object(
... web, 'VSFElasticsearch', return_value=es).start()
Install web_shop_vue_storefront::
>>> config = activate_modules(
... ['web_shop_vue_storefront', 'product_attribute', 'product_image'],
... create_company, create_chart)
Get accounts::
>>> accounts = get_accounts()
Define a web shop::
>>> WebShop = Model.get('web.shop')
>>> web_shop = WebShop(name="Web Shop")
>>> web_shop.type = 'vsf'
>>> web_shop.save()
Create categories::
>>> Category = Model.get('product.category')
>>> category1 = Category(name="Category 1")
>>> category1.save()
>>> sub_category = Category(name="Sub Category", parent=category1)
>>> sub_category.save()
>>> category2 = Category(name="Category 2")
>>> category2.save()
>>> account_category = Category(name="Account Category")
>>> account_category.accounting = True
>>> account_category.account_expense = accounts['expense']
>>> account_category.account_revenue = accounts['revenue']
>>> account_category.save()
Create attribute set::
>>> ProductAttributeSet = Model.get('product.attribute.set')
>>> ProductAttribute = Model.get('product.attribute')
>>> attribute_set = ProductAttributeSet(name="Attributes")
>>> attribute = attribute_set.attributes.new()
>>> attribute.name = 'attr1'
>>> attribute.string = "Attribute 1"
>>> attribute.type_ = 'selection'
>>> attribute.selection = "opt1:Option1\nopt2:Option2"
>>> attribute = attribute_set.attributes.new()
>>> attribute.name = 'attr2'
>>> attribute.string = "Attribute 1"
>>> attribute.type_ = 'boolean'
>>> attribute_set.save()
>>> attribute1, attribute2 = attribute_set.attributes
Create products::
>>> ProductUom = Model.get('product.uom')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> ProductTemplate = Model.get('product.template')
>>> Product = Model.get('product.product')
>>> template = ProductTemplate()
>>> template.name = "Product 1"
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.salable = True
>>> template.list_price = Decimal(10)
>>> template.account_category = account_category
>>> template.categories.append(Category(category1.id))
>>> template.categories.append(Category(sub_category.id))
>>> template.save()
>>> product1, = template.products
>>> product1.suffix_code = 'PROD1'
>>> image = product1.images.new()
>>> image.template = template
>>> image.image = pixel
>>> product1.save()
>>> template = ProductTemplate()
>>> template.name = "Product 2"
>>> template.default_uom = unit
>>> template.type = 'goods'
>>> template.salable = True
>>> template.list_price = Decimal(20)
>>> template.account_category = account_category
>>> template.save()
>>> product2, = template.products
>>> product2.suffix_code = 'PROD2'
>>> product2.save()
>>> configurable = ProductTemplate()
>>> configurable.name = "Configurable"
>>> configurable.code = "CONF"
>>> configurable.default_uom = unit
>>> configurable.type = 'goods'
>>> configurable.salable = True
>>> configurable.list_price = Decimal(50)
>>> configurable.attribute_set = attribute_set
>>> configurable.account_category = account_category
>>> image = configurable.images.new()
>>> image.image = pixel
>>> configurable1, = configurable.products
>>> configurable1.suffix_code = "1"
>>> configurable1.attributes = {
... 'attr1': 'opt1',
... 'attr2': True,
... }
>>> configurable2 = configurable.products.new()
>>> configurable2.suffix_code = "2"
>>> configurable2.attributes = {
... 'attr1': 'opt2',
... 'attr2': True,
... }
>>> configurable.save()
>>> configurable1, configurable2 = configurable.products
Set categories, products and attributes to web shop::
>>> web_shop.categories.extend([
... Category(category1.id),
... Category(sub_category.id),
... Category(category2.id)])
>>> web_shop.products.extend([
... Product(product1.id),
... Product(product2.id),
... Product(configurable1.id),
... Product(configurable2.id)])
>>> web_shop.attributes.extend([
... ProductAttribute(attribute1.id),
... ProductAttribute(attribute2.id)])
>>> web_shop.save()
Run VSF update::
>>> es.reset_mock()
>>> Cron = Model.get('ir.cron')
>>> cron_sync, = Cron.find([
... ('method', '=', 'web.shop|vsf_update'),
... ])
>>> cron_sync.click('run_once')
>>> es.index.call_count
8
>>> es.index.assert_any_call(
... id=category1.vsf_identifier.id, index='vue_storefront_catalog',
... doc_type='category', body=AnyDictWith({
... 'name': "Category 1",
... 'parent_id': None,
... 'url_key': 'category-1',
... 'url_path': 'category-1',
... 'level': 1,
... 'product_count': 1,
... 'children_data': [AnyDictWith({})],
... }))
>>> es.index.assert_any_call(
... id=sub_category.vsf_identifier.id, index='vue_storefront_catalog',
... doc_type='category', body=AnyDictWith({
... 'name': "Sub Category",
... 'parent_id': category1.vsf_identifier.id,
... 'url_key': 'sub-category',
... 'url_path': 'category-1/sub-category',
... 'level': 2,
... 'product_count': 1,
... 'children_data': [],
... }))
>>> es.index.assert_any_call(
... id=product1.vsf_identifier.id, index='vue_storefront_catalog',
... doc_type='product', body=AnyDictWith({
... 'name': "Product 1",
... 'image': ANY,
... 'sku': 'PROD1',
... 'url_key': 'product-1',
... 'type_id': 'simple',
... 'price': 10,
... 'price_tax': 0,
... 'price_incl_tax': 10,
... 'status': 3,
... 'category_ids': [ANY, ANY],
... 'category': [AnyDictWith({}), AnyDictWith({})],
... 'stock': [{
... 'is_in_stock': False,
... 'qty': 0,
... }],
... }))
>>> es.index.assert_any_call(
... id=configurable.vsf_identifier.id, index='vue_storefront_catalog',
... doc_type='product', body=AnyDictWith({
... 'name': "Configurable",
... 'image': ANY,
... 'sku': 'CONF',
... 'url_key': 'configurable',
... 'type_id': 'configurable',
... 'price': 50,
... 'price_tax': 0,
... 'price_incl_tax': 50,
... 'status': 3,
... 'category_ids': [],
... 'category': [],
... 'stock': [{
... 'is_in_stock': False,
... 'qty': 0,
... }],
... 'attr1_options': [1, 2],
... 'attr2_options': [],
... 'configurable_options': [
... AnyDictWith({
... 'attribute_code': 'attr1',
... 'label': "Attribute 1",
... 'product_id': configurable.vsf_identifier.id,
... 'values': [
... {'value_index': 1, 'label': "Option1"},
... {'value_index': 2, 'label': "Option2"},
... ],
... }),
... AnyDictWith({}),
... ],
... 'configurable_children': [
... AnyDictWith({'sku': 'CONF1'}),
... AnyDictWith({'sku': 'CONF2'}),
... ],
... }))
>>> es.index.assert_any_call(
... id=attribute1.vsf_identifier.id, index='vue_storefront_catalog',
... doc_type='attribute', body=AnyDictWith({
... 'attribute_code': 'attr1',
... 'frontend_input': 'selection',
... 'frontend_label': "Attribute 1",
... 'options': [
... {'value': 1, 'name': 'opt1', 'label': "Option1"},
... {'value': 2, 'name': 'opt2', 'label': "Option2"},
... ],
... }))
Remove a category, a product and an attribute::
>>> _ = web_shop.categories.pop(web_shop.categories.index(category2))
>>> _ = web_shop.products.pop(web_shop.products.index(product2))
>>> _ = web_shop.attributes.pop(web_shop.attributes.index(attribute2))
>>> web_shop.save()
Run VSF update::
>>> es.reset_mock()
>>> Cron = Model.get('ir.cron')
>>> cron_sync, = Cron.find([
... ('method', '=', 'web.shop|vsf_update'),
... ])
>>> cron_sync.click('run_once')
>>> es.index.call_count
5
>>> es.delete.call_count
3

View File

@@ -0,0 +1,910 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from contextlib import contextmanager
from decimal import Decimal
from unittest.mock import ANY, patch
from trytond.modules.company.tests import (
CompanyTestMixin, create_company, set_company)
from trytond.modules.web_shop_vue_storefront.exceptions import LoginException
from trytond.pool import Pool
from trytond.protocols.wrappers import exceptions
from trytond.tests.test_tryton import ModuleTestCase, with_transaction
from trytond.transaction import Transaction
CUSTOMER = {
'email': 'customer@example.com',
'firstname': 'John',
'lastname': 'Doe',
}
ADDRESS = {
'region': {
'region': None,
},
'country_id': "US",
'street': [
"Cliff Street",
"300"
],
'telephone': '+1 202-555-0148',
'postcode': '18503',
'city': "Scranton",
'firstname': CUSTOMER['firstname'],
'lastname': CUSTOMER['lastname'],
}
ADDRESS_COMPANY = ADDRESS.copy()
ADDRESS_COMPANY['company'] = 'Saber'
ADDRESS_COMPANY['vat_id'] = 'BE0500923836'
class WebVueStorefrontTestCase(CompanyTestMixin, ModuleTestCase):
'Test Web Shop Vue Storefront module'
module = 'web_shop_vue_storefront'
extras = [
'product_attribute',
'product_image',
'sale_promotion_coupon',
'sale_shipment_cost',
'carrier',
]
maxDiff = None
@contextmanager
def create_web_shop(self):
pool = Pool()
WebShop = pool.get('web.shop')
Location = pool.get('stock.location')
Party = pool.get('party.party')
company = create_company()
warehouse, = Location.search([('type', '=', 'warehouse')], limit=1)
guest_party = Party(name="Guest")
guest_party.save()
web_shop = WebShop(name="Web Shop")
web_shop.company = company
web_shop.currency = company.currency
web_shop.warehouses = [warehouse]
web_shop.type = 'vsf'
web_shop.guest_party = guest_party
web_shop.save()
with Transaction().set_context(**web_shop.get_context()):
yield web_shop
def create_product(self, web_shop, quantity=0, code="CODE"):
pool = Pool()
Template = pool.get('product.template')
Product = pool.get('product.product')
Category = pool.get('product.category')
Uom = pool.get('product.uom')
Location = pool.get('stock.location')
Move = pool.get('stock.move')
unit, = Uom.search([('name', '=', "Unit")], limit=1)
account_category = Category(name="Category", accounting=True)
account_category.save()
template = Template(name="Product")
template.type = 'goods'
template.list_price = Decimal(100)
template.default_uom = unit
template.salable = True
template.sale_uom = unit
template.products = [Product(suffix_code=code, web_shops=[web_shop])]
template.account_category = account_category
template.save()
product, = template.products
if quantity:
supplier, = Location.search([('code', '=', 'SUP')])
storage, = Location.search([('code', '=', 'STO')])
with set_company(web_shop.company):
move = Move(product=product)
move.unit = unit
move.quantity = quantity
move.from_location = supplier
move.to_location = storage
move.unit_price = Decimal(50)
move.currency = web_shop.company.currency
move.save()
Move.do([move])
Product.set_vsf_identifier([product])
return product
def create_coupon(self, code='CODE'):
pool = Pool()
Promotion = pool.get('sale.promotion')
Coupon = pool.get('sale.promotion.coupon')
promotion = Promotion(name="10%")
promotion.formula = '0.9 * unit_price'
promotion.coupons = coupon, = [Coupon()]
coupon.numbers = [{'number': code}]
promotion.save()
def create_carrier(
self, name='Carrier', product_name="Delivery",
price=Decimal('10')):
pool = Pool()
Carrier = pool.get('carrier')
Party = pool.get('party.party')
Template = pool.get('product.template')
Product = pool.get('product.product')
Category = pool.get('product.category')
Uom = pool.get('product.uom')
unit, = Uom.search([('name', '=', "Unit")], limit=1)
account_category = Category(name="Service", accounting=True)
account_category.save()
template = Template(name=product_name)
template.type = 'service'
template.list_price = price
template.default_uom = template.sale_uom = unit
template.salable = True
template.products = [Product()]
template.account_category = account_category
template.save()
party = Party(name=name)
party.save()
carrier = Carrier()
carrier.party = party
carrier.carrier_product, = template.products
carrier.save()
return carrier
@with_transaction(user=0)
def test_user_create(self):
pool = Pool()
User = pool.get('web.user')
with self.create_web_shop() as web_shop:
result = web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "SecretPassword"
})
users = User.search([])
self.assertEqual(result, {
"email": CUSTOMER['email'],
"firstname": CUSTOMER['firstname'],
"lastname": CUSTOMER['lastname'],
"addresses": [],
})
self.assertEqual(len(users), 1)
@with_transaction(user=0)
def test_user_login(self):
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
result = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
self.assertTrue(result)
@with_transaction(user=0)
def test_user_login_wrong_password(self):
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
with self.assertRaises(LoginException):
web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "Bad",
})
@with_transaction(user=0)
def test_user_reset_password(self):
pool = Pool()
User = pool.get('web.user')
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "SecretPassword",
})
with patch.object(User, 'reset_password') as reset_password:
result = web_shop.POST_vsf_user_reset_password({
"email": CUSTOMER['email'],
})
self.assertFalse(result)
reset_password.assert_called_once_with(ANY)
@with_transaction(user=0)
def test_user_change_password(self):
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "OldPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "OldPassword",
})
result = web_shop.POST_vsf_user_change_password({
"currentPassword": "OldPassword",
"newPassword": "NewPassword",
}, token=token)
self.assertFalse(result)
@with_transaction(user=0)
def test_user_change_password_wrong_token(self):
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "OldPassword",
})
with self.assertRaises(exceptions.Unauthorized):
web_shop.POST_vsf_user_change_password({
"currentPassword": "BadPassword",
"newPassword": "NewPassword",
}, 'wrong token')
@with_transaction(user=0)
def test_user_change_password_wrong_password(self):
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "OldPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "OldPassword",
})
with self.assertRaises(LoginException):
web_shop.POST_vsf_user_change_password({
"currentPassword": "BadPassword",
"newPassword": "NewPassword",
}, token=token)
@with_transaction(user=0)
def test_user_order_history(self):
pool = Pool()
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
cart = web_shop.POST_vsf_cart_create(None, token=token)
web_shop.POST_vsf_order_create({
'products': [{
'sku': 'CODE',
'qty': 2,
}],
'addressInformation': {
'shippingAddress': ADDRESS,
'billingAddress': ADDRESS_COMPANY,
'shipping_method_code': None,
'shipping_carrier_code': None,
'payment_method_code': 'cashondelivery',
'payment_method_additional': {},
},
}, cart, token=token)
items = web_shop.GET_vsf_user_order_history(None, token=token)
self.assertEqual(items, {
'items': [{
'entity_id': ANY,
'increment_id': ANY,
'created_at': ANY,
'customer_firstname': CUSTOMER['firstname'],
'customer_lastname': CUSTOMER['lastname'],
'grand_total': 200,
'status': "Confirmed",
'items': [{
'name': "Product",
'sku': 'CODE',
'price_incl_tax': 100,
'qty_ordered': 2,
'row_total_incl_tax': 200,
}],
}],
})
@with_transaction(user=0)
def test_user_order_history_wrong_token(self):
with self.create_web_shop() as web_shop:
with self.assertRaises(exceptions.Unauthorized):
web_shop.GET_vsf_user_order_history(None, 'wrong token')
@with_transaction(user=0)
def test_user_me(self):
pool = Pool()
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
result = web_shop.GET_vsf_user_me(None, token=token)
data = {'customer': CUSTOMER.copy()}
data['customer']['addresses'] = []
self.assertEqual(result, data['customer'])
data = {'customer': CUSTOMER.copy()}
data['customer']['addresses'] = [ADDRESS_COMPANY.copy()]
result = web_shop.POST_vsf_user_me(data, token=token)
data['customer']['addresses'][0]['id'] = ANY
data['customer']['addresses'][0].pop('region')
self.assertEqual(result, data['customer'])
@with_transaction(user=0)
def test_user_me_wrong_token(self):
with self.create_web_shop() as web_shop:
with self.assertRaises(exceptions.Unauthorized):
web_shop.GET_vsf_user_me(None, 'wrong token')
with self.assertRaises(exceptions.Unauthorized):
web_shop.POST_vsf_user_me({}, 'wrong token')
@with_transaction(user=0)
def test_user_me_change_address(self):
pool = Pool()
Address = pool.get('party.address')
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
data = {'customer': CUSTOMER.copy()}
data['customer']['addresses'] = [ADDRESS_COMPANY]
result = web_shop.POST_vsf_user_me(data, token=token)
data = {'customer': CUSTOMER.copy()}
address = ADDRESS.copy()
address['lastname'] = "Scott"
data['customer']['addresses'] = [address]
result = web_shop.POST_vsf_user_me(
{"customer": result}, token=token)
inactive_addresses = Address.search([('active', '=', False)])
self.assertEqual(len(inactive_addresses), 1)
@with_transaction(user=0)
def test_stock_check(self):
with self.create_web_shop() as web_shop:
product = self.create_product(web_shop, quantity=10)
result = web_shop.GET_vsf_stock_check(None, product.vsf_sku)
self.assertEqual(result, {
'product_id': product.vsf_identifier.id,
'qty': 10,
'is_in_stock': True,
})
@with_transaction(user=0)
def test_stock_list(self):
with self.create_web_shop() as web_shop:
product1 = self.create_product(web_shop, quantity=10)
product2 = self.create_product(web_shop, quantity=0, code="CODE2")
result = web_shop.GET_vsf_stock_list(
None, ','.join([p.vsf_sku for p in [product1, product2]]))
self.assertEqual(result, [{
'product_id': product1.vsf_identifier.id,
'qty': 10,
'is_in_stock': True,
}, {
'product_id': product2.vsf_identifier.id,
'qty': 0,
'is_in_stock': False,
}])
@with_transaction(user=0)
def test_cart_create_without_token(self):
with self.create_web_shop() as web_shop:
cart = web_shop.POST_vsf_cart_create(None)
self.assertIsInstance(cart, str)
@with_transaction(user=0)
def test_cart_create_with_token(self):
with self.create_web_shop() as web_shop:
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
cart = web_shop.POST_vsf_cart_create(None, token=token)
self.assertIsInstance(cart, int)
@with_transaction(user=0)
def test_cart_pull_without_token(self):
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
items = web_shop.GET_vsf_cart_pull(None, cart)
self.assertEqual(items, [{
'item_id': ANY,
'sku': 'CODE',
'qty': 1,
'name': 'Product',
'price': 100,
'product_type': 'simple',
'quote_id': ANY,
'product_option': {
'extension_attributes': {},
},
}])
@with_transaction(user=0)
def test_cart_pull_with_token(self):
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
cart = web_shop.POST_vsf_cart_create(None, token=token)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart, token=token)
items = web_shop.GET_vsf_cart_pull(None, cart, token=token)
self.assertEqual(items, [{
'item_id': ANY,
'sku': 'CODE',
'qty': 1,
'name': 'Product',
'price': 100,
'product_type': 'simple',
'quote_id': ANY,
'product_option': {
'extension_attributes': {},
},
}])
@with_transaction(user=0)
def test_cart_update_without_token(self):
pool = Pool()
SaleLine = pool.get('sale.line')
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
item = web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
self.assertEqual(item, {
'item_id': ANY,
'sku': 'CODE',
'qty': 1,
'name': 'Product',
'price': 100,
'product_type': 'simple',
'quote_id': ANY,
'product_option': {
'extension_attributes': {},
},
})
sale_lines = SaleLine.search([])
self.assertEqual(len(sale_lines), 1)
@with_transaction(user=0)
def test_cart_update_with_token(self):
pool = Pool()
SaleLine = pool.get('sale.line')
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
cart = web_shop.POST_vsf_cart_create(None, token=token)
item = web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart, token=token)
self.assertEqual(item, {
'item_id': ANY,
'sku': 'CODE',
'qty': 1,
'name': 'Product',
'price': 100,
'product_type': 'simple',
'quote_id': ANY,
'product_option': {
'extension_attributes': {},
},
})
sale_lines = SaleLine.search([])
self.assertEqual(len(sale_lines), 1)
@with_transaction(user=0)
def test_cart_update2(self):
pool = Pool()
SaleLine = pool.get('sale.line')
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
item = web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
item = web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 2,
'item_id': item['item_id'],
},
}, cart)
self.assertEqual(item, {
'item_id': ANY,
'sku': 'CODE',
'qty': 2,
'name': 'Product',
'price': 100,
'product_type': 'simple',
'quote_id': ANY,
'product_option': {
'extension_attributes': {},
},
})
sale_lines = SaleLine.search([])
self.assertEqual(len(sale_lines), 1)
@with_transaction(user=0)
def test_cart_delete(self):
pool = Pool()
SaleLine = pool.get('sale.line')
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
item = web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
result = web_shop.POST_vsf_cart_delete({
'cartItem': {
'sku': 'CODE',
'qty': 1,
'item_id': item['item_id'],
},
}, cart)
self.assertTrue(result)
sale_lines = SaleLine.search([])
self.assertEqual(len(sale_lines), 0)
@with_transaction(user=0)
def test_cart_apply_coupon(self):
with self.create_web_shop() as web_shop:
self.create_coupon()
cart = web_shop.POST_vsf_cart_create(None)
result = web_shop.POST_vsf_cart_apply_coupon(None, cart, 'CODE')
self.assertTrue(result)
@with_transaction(user=0)
def test_cart_delete_coupon(self):
with self.create_web_shop() as web_shop:
self.create_coupon()
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_apply_coupon(None, cart, 'CODE')
result = web_shop.POST_vsf_cart_delete_coupon(None, cart)
self.assertTrue(result)
@with_transaction(user=0)
def test_cart_coupon(self):
with self.create_web_shop() as web_shop:
self.create_coupon()
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_apply_coupon(None, cart, 'CODE')
result = web_shop.POST_vsf_cart_coupon(None, cart)
self.assertEqual(result, 'CODE')
@with_transaction(user=0)
def test_cart_coupon_empty(self):
with self.create_web_shop() as web_shop:
cart = web_shop.POST_vsf_cart_create(None)
result = web_shop.POST_vsf_cart_coupon(None, cart)
self.assertEqual(result, '')
@with_transaction(user=0)
def test_cart_totals(self):
with self.create_web_shop() as web_shop:
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
totals = web_shop.GET_vsf_cart_totals(None, cart)
self.assertEqual(totals, {
'grand_total': 100,
'items': [{
'item_id': ANY,
'sku': 'CODE',
'qty': 1,
'name': 'Product',
'price': 100,
'product_type': 'simple',
'quote_id': ANY,
'product_option': {
'extension_attributes': {
},
},
}],
'total_segments': [{
'code': 'subtotal',
'title': ANY,
'value': 100,
}, {
'code': 'tax',
'title': ANY,
'value': 0,
}, {
'code': 'grand_total',
'title': ANY,
'value': 100,
}],
})
@with_transaction(user=0)
def test_cart_payment_methods(self):
with self.create_web_shop() as web_shop:
cart = web_shop.POST_vsf_cart_create(None)
result = web_shop.GET_vsf_cart_payment_methods(None, cart)
self.assertEqual(result, [])
@with_transaction(user=0)
def test_cart_shipping_methods(self):
pool = Pool()
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
carrier = self.create_carrier()
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
methods = web_shop.POST_vsf_cart_shipping_methods(
{'address': {'country_id': 'US'}}, cart)
self.assertEqual(methods, [{
'carrier_code': str(carrier.id),
'method_code': str(carrier.id),
'carrier_title': "Carrier",
'method_title': "Delivery",
'price_incl_tax': Decimal(10),
}])
@with_transaction(user=0)
def test_cart_shipping_information(self):
pool = Pool()
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
carrier = self.create_carrier()
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
information = web_shop.POST_vsf_cart_shipping_information({
'addressInformation': {
'shipping_address': {
'country_id': 'US',
},
'shipping_method_code': str(carrier.id),
'shipping_carrier_code': str(carrier.id),
},
}, cart)
self.assertEqual(information, {
'grand_total': 110,
'items': ANY,
'total_segments': [{
'code': 'subtotal',
'title': ANY,
'value': 100,
}, {
'code': 'shipping',
'title': ANY,
'value': 10,
}, {
'code': 'tax',
'title': ANY,
'value': 0,
}, {
'code': 'grand_total',
'title': ANY,
'value': 110,
}],
})
@with_transaction(user=0)
def test_order_create(self):
pool = Pool()
Sale = pool.get('sale.sale')
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
carrier = self.create_carrier()
self.create_product(web_shop)
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
cart = web_shop.POST_vsf_cart_create(None, token=token)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart, token=token)
result = web_shop.POST_vsf_order_create({
'products': [{
'sku': 'CODE',
'qty': 1,
}],
'addressInformation': {
'shippingAddress': ADDRESS,
'billingAddress': ADDRESS_COMPANY,
'shipping_method_code': str(carrier.id),
'shipping_carrier_code': str(carrier.id),
'payment_method_code': 'cashondelivery',
'payment_method_additional': {},
},
}, cart, token=token)
self.assertEqual(result, 'OK')
sale, = Sale.search([])
self.assertEqual(sale.state, 'confirmed')
self.assertEqual(len(sale.lines), 2)
self.assertEqual(sale.total_amount, Decimal(110))
@with_transaction(user=0)
def test_order_create_from_guest(self):
pool = Pool()
Sale = pool.get('sale.sale')
Country = pool.get('country.country')
Country(name="US", code="US").save()
with self.create_web_shop() as web_shop:
carrier = self.create_carrier()
self.create_product(web_shop)
cart = web_shop.POST_vsf_cart_create(None)
web_shop.POST_vsf_cart_update({
'cartItem': {
'sku': 'CODE',
'qty': 1,
},
}, cart)
web_shop.POST_vsf_user_create({
"customer": CUSTOMER,
"password": "TopSecretPassword",
})
token = web_shop.POST_vsf_user_login({
"username": CUSTOMER['email'],
"password": "TopSecretPassword",
})
result = web_shop.POST_vsf_order_create({
'products': [{
'sku': 'CODE',
'qty': 1,
}],
'addressInformation': {
'shippingAddress': ADDRESS,
'billingAddress': ADDRESS_COMPANY,
'shipping_method_code': str(carrier.id),
'shipping_carrier_code': str(carrier.id),
'payment_method_code': 'cashondelivery',
'payment_method_additional': {},
},
}, cart, token=token)
self.assertEqual(result, 'OK')
sale, = Sale.search([])
self.assertNotEqual(sale.party, web_shop.guest_party)
self.assertEqual(sale.state, 'confirmed')
self.assertEqual(len(sale.lines), 2)
self.assertEqual(sale.total_amount, Decimal(110))
del ModuleTestCase

View File

@@ -0,0 +1,8 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from trytond.tests.test_tryton import load_doc_tests
def load_tests(*args, **kwargs):
return load_doc_tests(__name__, __file__, *args, **kwargs)

View File

@@ -0,0 +1,10 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
class AnyDictWith(dict):
def __eq__(self, other):
if not isinstance(other, dict):
return False
items = list(sorted(filter(lambda i: i[0] in self, other.items())))
return list(sorted(self.items())) == items

View File

@@ -0,0 +1,52 @@
[tryton]
version=7.8.0
depends:
ir
party
product
sale
web_shop
web_user
extras_depend:
carrier
product_attribute
product_image
sale_promotion_coupon
sale_shipment_cost
xml:
web.xml
message.xml
[register]
model:
ir.Cron
web.Shop
web.ShopVSFIdentifier
web.User
party.Address
party.Identifier
product.Product
product.Template
product.Category
sale.Sale
sale.Line
[register carrier]
model:
carrier.Carrier
[register product_attribute]
model:
product.ProductAttribute
product.TemplateAttribute
product.Attribute
sale.LineAttribute
[register sale_promotion_coupon]
model:
web.ShopCoupon
[register sale_shipment_cost]
model:
web.ShopShipmentCost
sale.SaleShipmentCost

View File

@@ -0,0 +1,13 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<data>
<xpath expr="//notebook/page[@id='products']" position="after">
<page string="Vue Storefront" id="vsf">
<label name="vsf_elasticsearch_url"/>
<field name="vsf_elasticsearch_url"/>
<label name="vsf_elasticsearch_index"/>
<field name="vsf_elasticsearch_index"/>
</page>
</xpath>
</data>

View File

@@ -0,0 +1,668 @@
# This file is part of Tryton. The COPYRIGHT file at the top level of
# this repository contains the full copyright notices and license terms.
from functools import wraps
import elasticsearch
from elasticsearch import VERSION as ES_VERSION
from elasticsearch import Elasticsearch
from trytond.exceptions import RateLimitException
from trytond.i18n import gettext, lazy_gettext
from trytond.model import ModelSQL, Unique, fields
from trytond.pool import Pool, PoolMeta
from trytond.pyson import Eval
from .exceptions import BadRequest, LoginException, NotFound
def migrate_doc_type(func):
@wraps(func)
def wrapper(*args, **kwargs):
if ES_VERSION >= (7,):
kwargs = kwargs.copy()
doc_type = kwargs.pop('doc_type')
kwargs['index'] += '_' + doc_type
return func(*args, **kwargs)
return wrapper
class VSFElasticsearch(Elasticsearch):
@migrate_doc_type
def index(self, **kwargs):
return super().index(**kwargs)
@migrate_doc_type
def delete(self, **kwargs):
return super().delete(**kwargs)
def join_name(firstname, lastname):
# Use unbreakable spaces in firstname
# to prevent to split on them
firstname = firstname.replace(' ', ' ')
return ' '.join([firstname, lastname])
def split_name(name):
return (name.split(' ', 1) + [''])[:2]
def issubdict(dct, other):
for key, value in dct.items():
if value != other.get(key):
return False
return True
def with_user(required=False):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
pool = Pool()
User = pool.get('web.user')
token = kwargs.pop('token', None)
if token:
user = User.get_user(token)
else:
user = kwargs.get('user')
if required and not user:
raise LoginException()
kwargs['user'] = user
return func(*args, **kwargs)
return wrapper
return decorator
class Shop(metaclass=PoolMeta):
__name__ = 'web.shop'
vsf_elasticsearch_url = fields.Char(
"Elasticsearch URL",
states={
'required': Eval('type') == 'vsf',
'invisible': Eval('type') != 'vsf',
})
vsf_elasticsearch_index = fields.Char(
"Elasticsearch Index",
states={
'required': Eval('type') == 'vsf',
'invisible': Eval('type') != 'vsf',
})
@classmethod
def __setup__(cls):
super().__setup__()
cls.type.selection.append(('vsf', "Vue Storefront"))
@classmethod
def default_vsf_elasticsearch_url(cls):
return 'http://localhost:9200/'
@classmethod
def default_vsf_elasticsearch_index(cls):
return 'vue_storefront_catalog'
@classmethod
def view_attributes(cls):
return super().view_attributes() + [
('//page[@id="vsf"]', 'states', {
'invisible': Eval('type') != 'vsf',
}),
]
@property
def to_sync(self):
result = super().to_sync
if self.type == 'vsf':
result = True
return result
def get_vsf_elasticsearch(self):
return VSFElasticsearch(self.vsf_elasticsearch_url)
@classmethod
def vsf_update(cls, shops=None):
pool = Pool()
Product = pool.get('product.product')
ProductTemplate = pool.get('product.template')
Category = pool.get('product.category')
try:
ProductAttribute = pool.get('product.attribute')
except KeyError:
ProductAttribute = None
if shops is None:
shops = cls.search([
('type', '=', 'vsf'),
])
cls.lock(shops)
for shop in shops:
es = shop.get_vsf_elasticsearch()
if ProductAttribute:
attributes = shop.get_attributes()
ProductAttribute.set_vsf_identifier(attributes)
categories = shop.get_categories()
Category.set_vsf_identifier(categories)
products, prices, taxes = shop.get_products()
Product.set_vsf_identifier(products)
templates = set()
for product in products:
if product.vsf_is_configurable(shop):
templates.add(product.template)
continue
entity = product.get_vsf_entity(
shop, price=prices[product.id], tax=taxes[product.id])
es.index(
index=shop.vsf_elasticsearch_index,
doc_type='product',
id=product.vsf_identifier.id,
body=entity)
templates = ProductTemplate.browse(templates)
ProductTemplate.set_vsf_identifier(templates)
for template in templates:
template_products = template.get_vsf_products(shop)
price, tax = min(
(prices[p.id], taxes[p.id]) for p in template_products
if prices[p.id] is not None and taxes[p.id] is not None)
entity = template.get_vsf_entity(shop, price=price, tax=tax)
entity['configurable_children'] = [
product.get_vsf_entity(
shop, price=prices[product.id], tax=taxes[product.id])
for product in template_products]
es.index(
index=shop.vsf_elasticsearch_index,
doc_type='product',
id=template.vsf_identifier.id,
body=entity)
for category in categories:
entity = category.get_vsf_entity(shop)
es.index(
index=shop.vsf_elasticsearch_index,
doc_type='category',
id=category.vsf_identifier.id,
body=entity)
if ProductAttribute:
for attribute in attributes:
entity = attribute.get_vsf_entity(shop)
es.index(
index=shop.vsf_elasticsearch_index,
doc_type='attribute',
id=attribute.vsf_identifier.id,
body=entity)
for product in shop.products_removed:
template = product.template
if template.vsf_identifier:
template_products = product.template.get_vsf_products(shop)
if not template_products:
try:
es.delete(
index=shop.vsf_elasticsearch_index,
doc_type='product',
id=template.vsf_identifier.id)
except elasticsearch.exceptions.NotFoundError:
pass
if product.vsf_identifier:
try:
es.delete(
index=shop.vsf_elasticsearch_index,
doc_type='product',
id=product.vsf_identifier.id)
except elasticsearch.exceptions.NotFoundError:
pass
shop.products_removed = []
for category in shop.categories_removed:
if category.vsf_identifier:
try:
es.delete(
index=shop.vsf_elasticsearch_index,
doc_type='category',
id=category.vsf_identifier.id)
except elasticsearch.exceptions.NotFoundError:
pass
shop.categories_removed = []
if ProductAttribute:
for attribute in shop.attributes_removed:
if attribute.vsf_identifier:
try:
es.delete(
index=shop.vsf_elasticsearch_index,
doc_type='attribute',
id=attribute.vsf_identifier.id)
except elasticsearch.exceptions.NotFoundError:
pass
shop.attributes_removed = []
cls.save(shops)
def POST_vsf_user_create(self, data):
pool = Pool()
User = pool.get('web.user')
Party = pool.get('party.party')
firstname = data['customer']['firstname']
lastname = data['customer']['lastname']
email = data['customer']['email']
user = User(email=email, password=data['password'])
party = Party(name=join_name(firstname, lastname))
party.save()
user.party = party
user.save()
firstname, lastname = split_name(user.party.name)
return {
'email': user.email,
'firstname': firstname,
'lastname': lastname,
'addresses': [],
}
def POST_vsf_user_login(self, data):
pool = Pool()
User = pool.get('web.user')
try:
user = User.authenticate(data['username'], data['password'])
except RateLimitException:
raise LoginException(gettext(
'web_shop_vue_storefront.msg_login_wrong'))
if user:
return user.new_session()
else:
raise LoginException(gettext(
'web_shop_vue_storefront.msg_login_wrong'))
def POST_vsf_user_reset_password(self, data):
pool = Pool()
User = pool.get('web.user')
users = User.search([
('email', '=', data['email']),
])
User.reset_password(users)
@with_user(required=True)
def POST_vsf_user_change_password(self, data, user):
pool = Pool()
User = pool.get('web.user')
try:
user = User.authenticate(user.email, data['currentPassword'])
except RateLimitException:
raise LoginException(gettext(
'web_shop_vue_storefront.msg_login_wrong'))
if user:
user.password = data['newPassword']
user.save()
else:
raise LoginException(gettext(
'web_shop_vue_storefront.msg_login_wrong'))
@with_user(required=True)
def GET_vsf_user_order_history(
self, data, user, pageSize='20', currentPage='1'):
pool = Pool()
Sale = pool.get('sale.sale')
try:
pageSize = int(pageSize)
currentPage = int(currentPage)
except ValueError:
raise BadRequest()
sales = Sale.search([
('party', '=', user.party),
('state', 'in', ['confirmed', 'processing', 'done']),
],
offset=pageSize * (currentPage - 1),
limit=pageSize,
order=[
('sale_date', 'DESC'),
('id', 'DESC'),
])
items = []
for sale in sales:
items.append(sale.get_vsf_user_order_history())
return {'items': items}
@with_user(required=True)
def GET_vsf_user_me(self, data, user):
return user.get_vsf()
@with_user(required=True)
def POST_vsf_user_me(self, data, user):
user.set_vsf(data['customer'])
user.save()
return user.get_vsf()
def GET_vsf_stock_check(self, data, sku):
try:
return self.GET_vsf_stock_list(data, sku)[0]
except IndexError:
raise NotFound()
def GET_vsf_stock_list(self, data, skus):
pool = Pool()
Product = pool.get('product.product')
Template = pool.get('product.template')
skus = skus.split(',')
products = Product.search([
('vsf_sku', 'in', skus),
('web_shops', '=', self.id),
])
products += Template.search([
('vsf_sku', 'in', skus),
('products.web_shops', '=', self.id),
])
return [p.get_vsf_stock() for p in products]
@with_user()
def POST_vsf_cart_create(self, data, user=None):
party = user.party if user else None
sale = self.get_sale(party)
sale.save()
return sale.vsf_id
@with_user()
def GET_vsf_cart_pull(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
return [line.get_vsf() for line in sale.lines if line.product]
@with_user()
def POST_vsf_cart_update(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
SaleLine = pool.get('sale.line')
sale = Sale.search_vsf(cartId, self, user)
if data['cartItem'].get('item_id'):
line = SaleLine(data['cartItem']['item_id'])
if line.sale != sale:
raise BadRequest()
else:
line = SaleLine(sale=sale)
line.set_vsf(data['cartItem'])
line.save()
return line.get_vsf()
@with_user()
def POST_vsf_cart_delete(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
SaleLine = pool.get('sale.line')
sale = Sale.search_vsf(cartId, self, user)
line = SaleLine(data['cartItem']['item_id'])
if line.sale != sale:
raise BadRequest()
SaleLine.delete([line])
return True
@with_user()
def GET_vsf_cart_totals(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
return sale.get_vsf()
@with_user()
def GET_vsf_cart_payment_methods(self, data, cartId, user=None):
return []
@with_user()
def POST_vsf_cart_shipping_methods(self, data, cartId, user=None):
return []
@with_user()
def POST_vsf_cart_shipping_information(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
return sale.get_vsf()
@with_user(required=True)
def POST_vsf_order_create(self, data, cartId, user):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
self.vsf_order_create(data, sale, user)
return 'OK'
def vsf_order_create(self, data, sale, user):
pool = Pool()
Sale = pool.get('sale.sale')
SaleLine = pool.get('sale.line')
sale.set_vsf(data, user)
sku2lines = {
line.product.vsf_sku: line for line in sale.lines if line.product}
for product in data.get('products', []):
sku = product['sku']
line = sku2lines.get(sku)
if not line:
line = SaleLine(sale=sale)
sku2lines[sku] = line
line.set_vsf(product)
sale.lines = sku2lines.values()
sale.save()
Sale.quote([sale])
payment_method = data['addressInformation']['payment_method_code']
if payment_method == 'cashondelivery':
Sale.confirm([sale])
return sale
class ShopCoupon(metaclass=PoolMeta):
__name__ = 'web.shop'
@with_user()
def POST_vsf_cart_apply_coupon(self, data, cartId, coupon, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
PromotionCouponNumber = pool.get('sale.promotion.coupon.number')
try:
coupon, = PromotionCouponNumber.search([
('number', 'ilike', coupon),
], limit=1)
except ValueError:
return False
sale.coupons = [coupon]
sale.save()
return True
@with_user()
def POST_vsf_cart_delete_coupon(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
sale.coupons = []
sale.save()
return True
@with_user()
def POST_vsf_cart_coupon(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
if sale.coupons:
return sale.coupons[0].number
else:
return ''
class ShopShipmentCost(metaclass=PoolMeta):
__name__ = 'web.shop'
@with_user()
def POST_vsf_cart_shipping_methods(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
methods = super().POST_vsf_cart_shipping_methods(
data, cartId, user=user)
sale = Sale.search_vsf(cartId, self, user)
sale.set_vsf_shipping_methods(data)
for carrier in sale.available_carriers:
method = carrier.get_vsf()
sale.carrier = carrier
method['price_incl_tax'] = sale.compute_shipment_cost(carrier)
methods.append(method)
return methods
@with_user()
def POST_vsf_cart_shipping_information(self, data, cartId, user=None):
pool = Pool()
Sale = pool.get('sale.sale')
sale = Sale.search_vsf(cartId, self, user)
sale.set_vsf(data, user)
sale.save()
return super().POST_vsf_cart_shipping_information(
data, cartId, user=user)
class ShopVSFIdentifier(ModelSQL):
__name__ = 'web.shop.vsf_identifier'
record = fields.Reference("Record", 'get_records', required=True)
@classmethod
def __setup__(cls):
super().__setup__()
t = cls.__table__()
cls._sql_constraints = [
('record_unique', Unique(t, t.record),
'web_shop_vue_storefront.msg_identifier_record_unique'),
]
@classmethod
def get_records(cls):
pool = Pool()
Model = pool.get('ir.model')
get_name = Model.get_name
models = (klass.__name__ for _, klass in pool.iterobject()
if issubclass(klass, ShopVSFIdentifierMixin))
return [(m, get_name(m)) for m in models]
class ShopVSFIdentifierMixin:
__slots__ = ()
vsf_identifier = fields.Many2One(
'web.shop.vsf_identifier',
lazy_gettext('web_shop_vue_storefront.msg_vsf_identifier'),
ondelete='RESTRICT', readonly=True)
@classmethod
def set_vsf_identifier(cls, records):
pool = Pool()
Identifier = pool.get('web.shop.vsf_identifier')
vsf_identifiers = []
for record in records:
if not record.vsf_identifier:
record.vsf_identifier = Identifier(record=record)
vsf_identifiers.append(record.vsf_identifier)
Identifier.save(vsf_identifiers)
cls.save(records)
class User(metaclass=PoolMeta):
__name__ = 'web.user'
def get_vsf(self):
if not self.party:
return {
'email': self.email,
}
firstname, lastname = split_name(self.party.name)
data = {
'email': self.email,
'firstname': firstname,
'lastname': lastname,
'addresses': (
[a.get_vsf() for a in self.party.addresses]
+ [a.get_vsf(self.party) for p in self.secondary_parties
for a in p.addresses if p != self.party]),
}
default_billing = self.invoice_address
if not default_billing:
default_billing = self.party.address_get('invoice')
if default_billing:
data['default_billing'] = default_billing.id
default_shipping = self.shipment_address
if not default_shipping:
default_shipping = self.party.address_get('delivery')
if default_shipping:
data['default_shipping'] = default_shipping.id
return data
def set_vsf(self, data):
pool = Pool()
Party = pool.get('party.party')
Address = pool.get('party.address')
self.email = data['email']
if not self.party:
self.party = Party()
self.party.name = join_name(data['firstname'], data['lastname'])
default_billing = None
default_shipping = None
addresses = []
for address_data in data['addresses']:
address = self.set_vsf_address(address_data, self.party)
addresses.append(address)
if ((address.id and address.id == data.get('default_billing'))
or address_data.get('default_billing')):
default_billing = address
if ((address.id and address.id == data.get('default_shipping'))
or address_data.get('default_shipping')):
default_shipping = address
if address_data.get('id'):
if address_data['id'] != address.id:
address = Address(address_data['id'])
if (address.party != self.party
and address.party not in self.secondary_parties):
raise BadRequest()
address.active = False
addresses.append(address)
Address.save(addresses)
self.invoice_address = default_billing
self.shipment_address = default_shipping
def set_vsf_address(self, address_data, party):
pool = Pool()
Party = pool.get('party.party')
Address = pool.get('party.address')
Identifier = pool.get('party.identifier')
addresses = self.party.addresses
for party in self.secondary_parties:
addresses += party.addresses
for address in addresses:
if issubdict(address.get_vsf(party), address_data):
return address
address = Address()
party = address.party = self.party
if address_data.get('company'):
for company_party in self.secondary_parties:
tax_code = (
company_party.tax_identifier.code
if company_party.tax_identifier else '')
if (company_party.name == address_data['company']
and (not address_data.get('vat_id')
or tax_code == address_data['vat_id'])):
break
else:
identifier = Identifier()
identifier.set_vsf_tax_identifier(
address_data['vat_id'])
company_party = Party(
name=address_data['company'],
identifiers=[identifier])
company_party.save()
self.secondary_parties += (company_party,)
self.save()
address.party = company_party
address.set_vsf(address_data, party)
return address

View File

@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- This file is part of Tryton. The COPYRIGHT file at the top level of
this repository contains the full copyright notices and license terms. -->
<tryton>
<data>
<record model="ir.ui.view" id="shop_view_form">
<field name="model">web.shop</field>
<field name="inherit" ref="web_shop.shop_view_form"/>
<field name="name">shop_form</field>
</record>
</data>
<data noupdate="1">
<record model="ir.cron" id="cron_update">
<field name="method">web.shop|vsf_update</field>
<field name="interval_number" eval="1"/>
<field name="interval_type">days</field>
</record>
</data>
</tryton>