1
+ import base64
2
+
1
3
from odoo import api , models , fields , _
4
+
5
+ from odoo .exceptions import UserError
2
6
from odoo .tools import html2plaintext
3
7
4
8
5
9
class StockPicking (models .Model ):
6
10
7
11
_inherit = 'stock.picking'
8
12
9
- l10n_uy_cfe_id = fields . Many2one ( "l10n_uy_edi.document" , string = "Uruguay E-Resguardo CFE" , copy = False )
13
+ # Need to make it work with document types
10
14
l10n_latam_document_type_id = fields .Many2one ('l10n_latam.document.type' , string = 'Document Type (UY)' , copy = False )
11
- l10n_latam_document_number = fields .Char (string = 'Document Number (UY)' , readonly = True , states = {'draft' : [('readonly' , False )]}, copy = False )
15
+ l10n_latam_document_number = fields .Char (string = 'Document Number (UY)' , readonly = True , copy = False )
16
+ l10n_latam_available_document_type_ids = fields .Many2many ('l10n_latam.document.type' , compute = '_compute_l10n_latam_available_document_types' )
12
17
13
- # Fields that need to be fill before creating the CFE
14
- l10n_uy_edi_cfe_uuid = fields .Char (
15
- 'Key or UUID CFE' , help = "Unique identification per CFE in UCFE. Currently is formed by the concatenation of model name initials plust record id" , copy = False )
18
+ # Need to make it work with EDI (simil to what we have in account.move)
19
+ l10n_uy_edi_document_id = fields .Many2one ("l10n_uy_edi.document" , string = "Uruguay E-Invoice CFE" , copy = False )
20
+ l10n_uy_edi_cfe_uuid = fields .Char (related = "l10n_uy_edi_document_id.uuid" )
21
+ l10n_uy_edi_cfe_state = fields .Selection (related = "l10n_uy_edi_document_id.state" , store = True )
22
+ l10n_uy_edi_error = fields .Text (related = "l10n_uy_edi_document_id.message" )
23
+ l10n_uy_is_cfe = fields .Boolean (
24
+ compute = "_compute_l10n_uy_is_cfe" ,
25
+ help = "Campo tecnico para saber si es un comprobante electronico o no y usarlo en la vista para mostrar o requerir ciertos campos."
26
+ " por los momentos lo estamos usando solo para remitos pero podemos extenderlo para otros modelos"
27
+ )
16
28
l10n_uy_edi_addenda_ids = fields .Many2many (
17
- 'l10n_uy_edi.addenda' , string = "Addenda & Disclosure" ,
18
- domain = "[('type', 'in', ['issuer', 'receiver', 'cfe_doc', 'addenda'])]" )
19
-
20
- l10n_latam_available_document_type_ids = fields .Many2many ('l10n_latam.document.type' , compute = '_compute_l10n_latam_available_document_types' )
29
+ "l10n_uy_edi.addenda" ,
30
+ string = "Addenda & Disclosure" ,
31
+ domain = "[('type', 'in', ['issuer', 'receiver', 'cfe_doc', 'addenda'])]" ,
32
+ help = "Addendas and Mandatory Disclosure to add on the CFE. They can be added either to the issuer, receiver,"
33
+ " cfe doc additional info section or to the addenda section. However, the item type should not be set in"
34
+ " this field; instead, it should be specified in the invoice lines." )
35
+ l10n_uy_edi_cfe_sale_mode = fields .Selection ([
36
+ ("1" , "General Regime" ),
37
+ ("2" , "Consignment" ),
38
+ ("3" , "Reviewable Price" ),
39
+ ("4" , "Own goods to customs exclaves" ),
40
+ ("90" , "General Regime - exportation of services" ),
41
+ ("99" , "Other transactions" ),
42
+ ], "Sales Modality" , help = "This field is used in the XML to create an Export e-Invoice" )
43
+ l10n_uy_edi_cfe_transport_route = fields .Selection ([
44
+ ("1" , "Maritime" ),
45
+ ("2" , "Air" ),
46
+ ("3" , "Ground" ),
47
+ ("8" , "N/A" ),
48
+ ("9" , "Other" ),
49
+ ], "Transportation Route" , help = "This field is used in the XML to create an Export e-Invoice" )
50
+
51
+ # New fields only for pickings
21
52
l10n_uy_transfer_of_goods = fields .Selection (
22
- [('1' , 'Venta' ), ('2' , 'Traslados internos' )],
53
+ [('1' , 'Venta' ),
54
+ ('2' , 'Traslados internos' )],
23
55
string = "Traslados de Bienes" ,
24
56
)
25
-
26
- l10n_uy_cfe_sale_mod = fields .Selection ([
27
- ('1' , 'General Regime' ),
28
- ('2' , 'Consignment' ),
29
- ('3' , 'Reviewable Price' ),
30
- ('4' , 'Own goods to customs exclaves' ),
31
- ('90' , 'General Regime - exportation of services' ),
32
- ('99' , 'Other transactions' ),
33
- ], 'Sales Modality' , help = "This field is used in the XML to create an Export e-Delivery Guide" )
34
- l10n_uy_cfe_transport_route = fields .Selection ([
35
- ('1' , 'Maritime' ),
36
- ('2' , 'Air' ),
37
- ('3' , 'Ground' ),
38
- ('8' , 'N/A' ),
39
- ('9' , 'Other' ),
40
- ], 'Transportation Route' , help = "This field is used in the XML to create an Export e-Delivery Guide" )
41
- l10n_uy_place_of_delivery = fields .Char (
42
- "Place of Delivery" ,
43
- size = 100 ,
44
- help = "Indicación de donde se entrega la mercadería o se presta el servicio (Dirección, Sucursal, Puerto, etc,)" )
45
57
l10n_uy_edi_place_of_delivery = fields .Boolean (
46
58
"Place of Delivery" ,
59
+ size = 100 ,
47
60
help = "CFE: Indication of where the merchandise is delivered or the service is provided"
48
61
" (Address, Branch, Port, etc.) if True then we will inform the shipping address's name and street" )
49
62
63
+ # TODO KZ campo similar a lo que tenemos en el UX, revisar como se llama y usarlo igual
64
+ l10n_uy_cfe_xml = fields .Text ()
65
+
50
66
def name_get (self ):
51
- """ Display: 'Stock Picking Internal Sequence : Remito (if defined)' """
67
+ """ Display: 'Stock Picking Internal Sequence : Remito Number (if defined)' """
52
68
res = []
53
69
for rec in self :
54
70
if rec .l10n_latam_document_number :
@@ -58,31 +74,31 @@ def name_get(self):
58
74
res .append ((rec .id , name ))
59
75
return res
60
76
77
+ def _compute_l10n_uy_is_cfe (self ):
78
+ self .l10n_uy_is_cfe = False
79
+ if self .l10n_uy_edi_document_id .move_id .journal_id .l10n_uy_edi_type == 'electronic' :
80
+ self .l10n_uy_is_cfe = True
81
+
61
82
@api .depends ('partner_id' , 'company_id' , 'picking_type_code' )
62
83
def _compute_l10n_latam_available_document_types (self ):
63
84
uy_remitos = self .filtered (lambda x : x .country_code == 'UY' and x .picking_type_code == 'outgoing' )
64
-
65
85
uy_remitos .l10n_latam_available_document_type_ids = self .env ['l10n_latam.document.type' ].search (
66
86
self ._get_l10n_latam_documents_domain ())
67
87
(self - uy_remitos ).l10n_latam_available_document_type_ids = False
68
88
69
89
def _get_l10n_latam_documents_domain (self ):
90
+ """ return domain """
70
91
codes = self ._l10n_uy_get_remito_codes ()
71
92
return [('code' , 'in' , codes ), ('active' , '=' , True ), ('internal_type' , '=' , 'stock_picking' )]
72
93
73
- # TODO KZ evaluar si estaria bueno tener un boolean como este l10n_cl_draft_status
74
- # TODO KZ evaluar si agregar una constrains de unicidad para remitos, aplicaria para:
75
- # 1. remitos manual o preimpresos (no electronico),
76
- # 2. remitos generados en uruware y pasados a mano luego a oodo
77
- # 3. remitos de proveedor? no se si los necesitamos registrar
78
-
79
94
def action_cancel (self ):
80
- # The move cannot be modified once the CFE has been accepted by the DGI
95
+ """ El remito no puede ser modificado una vez que ya fue aceptado por DGI """
81
96
remitos = self .filtered (lambda x : x .country_code == 'UY' and x .picking_type_code == 'outgoing' )
82
- remitos ._uy_check_state ()
97
+ if remitos .filtered (lambda x : x .l10n_uy_edi_cfe_state in ['accepted' , 'rejected' , 'received' ]):
98
+ raise UserError (_ ('Can not cancel a Remito already process by DGI' ))
83
99
return super ().action_cancel ()
84
100
85
- def uy_post_dgi_remito (self ):
101
+ def l10n_uy_edi_stock_post_dgi (self ):
86
102
""" El E-remito tiene las siguientes partes en el xml
87
103
A. Encabezado
88
104
B. Detalle de los productos
@@ -91,15 +107,16 @@ def uy_post_dgi_remito(self):
91
107
"""
92
108
# Filtrar solo los e-remitos
93
109
uy_remitos = self .filtered (
94
- lambda x : x .country_code == 'UY' and x .picking_type_code == 'outgoing'
110
+ lambda x : x .country_code == 'UY'
111
+ and x .picking_type_code == 'outgoing'
95
112
and x .l10n_latam_document_type_id
96
113
and int (x .l10n_latam_document_type_id .code ) > 0
97
114
and x .l10n_uy_edi_cfe_state not in ['accepted' , 'rejected' , 'received' ]
98
115
)
99
116
100
- # If the invoice was previosly validated in Uruware and need to be link to Odoo we check that the
101
- # l10n_uy_edi_cfe_uuid has been manually set and we consult to get the invoice information from Uruware
102
- pre_validated_in_uruware = uy_remitos .filtered (lambda x : x .l10n_uy_edi_cfe_uuid and not x .l10n_uy_cfe_file and not x .l10n_uy_edi_cfe_state )
117
+ # If the invoice was previously validated in Uruware and need to be link to Odoo
118
+ # we check that the l10n_uy_edi_cfe_uuid has been manually set and we consult to get the invoice information from Uruware
119
+ pre_validated_in_uruware = uy_remitos .filtered (lambda x : x .l10n_uy_edi_cfe_uuid and not x .attachment_id and not x .l10n_uy_edi_cfe_state )
103
120
if pre_validated_in_uruware :
104
121
pre_validated_in_uruware .uy_ux_action_get_uruware_cfe ()
105
122
uy_remitos = uy_remitos - pre_validated_in_uruware
@@ -110,38 +127,99 @@ def uy_post_dgi_remito(self):
110
127
# Send invoices to DGI and get the return info
111
128
for remito in uy_remitos :
112
129
if remito .company_id .l10n_uy_edi_ucfe_env == "demo" :
113
- remito ._uy_dummy_validation ()
114
- continue
115
-
116
- # TODO KZ I think we can avoid this loop. review
117
- remito ._uy_dgi_post ()
130
+ attachments = remito ._l10n_uy_edi_dummy_validation ()
131
+ msg = _ (
132
+ "This CFE has been generated in DEMO Mode. It is considered"
133
+ " as accepted and it won\" t be sent to DGI." )
134
+ else :
135
+ remito ._uy_dgi_post ()
136
+ msg += _ ("The electronic invoice was created successfully" )
137
+
138
+ remito .with_context (no_new_invoice = True ).message_post (
139
+ body = msg , attachment_ids = attachments .ids if attachments else False )
140
+
141
+ def _l10n_uy_edi_dummy_validation (self ):
142
+ # COPY l10n_uy_edi (only change move_id with picking_id)
143
+ """ When we want to skip DGI and validate only in Odoo """
144
+ edi_doc = self .l10n_uy_edi_document_id
145
+ edi_doc .state = "accepted"
146
+ self .write ({
147
+ "l10n_latam_document_number" : "DE%07d" % (edi_doc .picking_id .id ),
148
+ "ref" : "*DEMO" ,
149
+ })
150
+
151
+ return self ._l10n_uy_edi_get_preview_xml ()
152
+
153
+ def _l10n_uy_edi_get_preview_xml (self ):
154
+ # COPY l10n_uy_edi
155
+ self .ensure_one ()
156
+ edi_doc = self .l10n_uy_edi_document_id
157
+ edi_doc .attachment_id .res_field = False
158
+ xml_file = self .env ["ir.attachment" ].create ({
159
+ "res_model" : "l10n_uy_edi.document" ,
160
+ "res_field" : "attachment_file" ,
161
+ "res_id" : edi_doc .id ,
162
+ "name" : edi_doc ._get_xml_attachment_name (),
163
+ "type" : "binary" ,
164
+ "datas" : base64 .b64encode (self ._l10n_uy_edi_get_xml_content ().encode ()),
165
+ })
166
+ edi_doc .invalidate_recordset (["attachment_id" , "attachment_file" ])
167
+ return xml_file
168
+
169
+ def _l10n_uy_edi_get_xml_content (self ):
170
+ # COPY l10n_uy_edi
171
+ """ Create the CFE xml structure and validate it
172
+ :return: string the xml content to send to DGI """
173
+ self .ensure_one ()
118
174
119
- # TODO KZ buscar el metodo _l10n_cl_get_tax_amounts para ejemplos de como extraer la info de los impuestos en un picking. viene siempre de una
120
- # factura
175
+ values = {
176
+ "cfe" : self ,
177
+ "IdDoc" : self ._l10n_uy_edi_cfe_A_iddoc (),
178
+ "emisor" : self ._l10n_uy_edi_cfe_A_issuer (),
179
+ "receptor" : self ._l10n_uy_edi_cfe_A_receptor (),
180
+ "item_detail" : self ._l10n_uy_edi_cfe_B_details (),
181
+ "totals_detail" : self ._l10n_uy_edi_cfe_C_totals (),
182
+ "referencia_lines" : self ._l10n_uy_edi_cfe_F_reference (),
183
+ "format_float" : format_float ,
184
+ }
185
+ cfe = self .env ["ir.qweb" ]._render (
186
+ "l10n_uy_edi." + self .l10n_uy_edi_document_id ._get_cfe_tag (self ) + "_template" , values )
187
+ return etree .tostring (cleanup_xml_node (cfe )).decode ()
188
+
189
+ def _l10n_uy_edi_get_addenda (self ):
190
+ """ return string with the addenda of the remito """
191
+ addenda = self .l10n_uy_edi_document_id ._get_legends ("addenda" , self )
192
+ if self .origin :
193
+ addenda += "\n \n Origin: %s" % self .origin
194
+ if self .note :
195
+ addenda += "\n \n %s" % html2plaintext (self .note )
196
+ return addenda .strip ()
121
197
122
- def _uy_get_cfe_addenda (self ):
123
- """ Add Specific MOVE model fields to the CFE Addenda if they are set:
198
+ def _l10n_uy_edi_get_used_rate (self ):
199
+ # COPY l10n_uy_edi
200
+ self .ensure_one ()
201
+ # We need to use abs to avoid error on Credit Notes (amount_total_signed is negative)
202
+ return abs (self .amount_total_signed ) / self .amount_total
124
203
125
- * field Origin added with the prefix "Origin: ..."
126
- * Observation
127
- """
204
+ def _l10n_uy_edi_cfe_C_totals (self ):
128
205
self .ensure_one ()
129
- res = super ()._uy_get_cfe_addenda ()
130
- if self .origin :
131
- res += "\n \n Origin: %s" % self .origin
132
- if self .note :
133
- res += "\n \n %s" % html2plaintext (self .note )
134
- return res .strip ()
206
+ currency_name = self .currency_id .name if self .currency_id else self .company_id .currency_id .name
207
+ lines = self ._uy_get_cfe_lines ()
208
+ res = {
209
+ 'TpoMoneda' : currency_name if not self ._is_uy_remito_loc () else None , # A110
210
+ 'TpoCambio' : None if currency_name == "UYU" else self ._l10n_uy_edi_get_used_rate () or None , # A111
211
+ 'CantLinDet' : len (lines ), # A126
212
+ }
213
+ return res
135
214
136
215
def _uy_get_cfe_lines (self ):
137
216
self .ensure_one ()
138
- if self ._is_uy_remito_type_cfe ():
139
- # TODO KZ: Toca revisar realmente cual es el line que corresponde, el que veo en la interfaz parece ser move_ids_without_package pero no se si esto siempre aplica
217
+ # TODO KZ: Toca revisar realmente cual es el line que corresponde, el que veo en la interfaz parece ser move_ids_without_package pero no se si esto siempre aplica
140
218
141
- # move_ids_without_package Stock moves not in package (stock.move)
142
- # move_line_ids Operations (stock.move.line)
143
- # move_line_ids_without_package Operations without package (stock.move.line)
144
- return self .move_ids_without_package
219
+ # move_ids_without_package Stock moves not in package (stock.move)
220
+ # move_line_ids Operations (stock.move.line)
221
+ # move_line_ids_without_package Operations without package (stock.move.line)
222
+ return self .move_ids_without_package
145
223
146
224
def _l10n_uy_get_remito_codes (self ):
147
225
""" return list of the available document type codes for uruguayan of stock picking"""
@@ -191,27 +269,3 @@ def _l10n_uy_edi_cfe_A_iddoc(self):
191
269
192
270
return res
193
271
194
-
195
- # TODO KZ este metodo debemos adaptarlo para obtener el IndFact
196
- def _uy_cfe_B4_IndFact (self , line ):
197
- """ B4: Indicador de facturación
198
-
199
- TODO KZ: Toca revisar realmente cual es el line que corresponde, el que veo en la interfaz parece ser move_ids_without_package pero no se si esto siempre aplica
200
- move_ids_without_package Stock moves not in package (stock.move)
201
- move_line_ids Operations (stock.move.line)
202
- move_line_ids_without_package Operations without package (stock.move.line)
203
- """
204
- # Another cases for future
205
- # 4: Gravado a Otra Tasa/IVA sobre fictos
206
- # 5: Entrega Gratuita. Por ejemplo docenas de trece
207
- # 6: Producto o servicio no facturable. No existe validación, excepto si A-C20= 1, B-C4=6 o 7.
208
- # 7: Producto o servicio no facturable negativo. . No existe validación, excepto si A-C20= 1, B-C4=6 o 7.
209
- # 8: Sólo para remitos: Ítem a rebajar en e-remitos y en e- remitos de exportación. En área de referencia se debe indicar el N° de remito que ajusta
210
- # 9: Sólo para resguardos: Ítem a anular en resguardos. En área de referencia se debe indicar el N° de resguardo que anular
211
- # 11: Impuesto percibido
212
- # 12: IVA en suspenso
213
- # 13: Sólo para e-Boleta de entrada y sus notas de corrección: Ítem vendido por un no contribuyente (valida que A-C60≠2)
214
- # 14: Sólo para e-Boleta de entrada y sus notas de corrección: Ítem vendido por un contribuyente IVA mínimo, Monotributo o Monotributo MIDES (valida que A-C60=2)
215
- # 15: Sólo para e-Boleta de entrada y sus notas de corrección: Ítem vendido por un contribuyente IMEBA (valida A-C60 = 2)
216
- # 16: Sólo para ítems vendidos por contribuyentes con obligación IVA mínimo, Monotributo o Monotributo MIDES. Si A-C10=3, no puede utilizar indicadores 1, 2, 3, 4, 11 ni 12
217
- return super ()._uy_cfe_B4_IndFact (line )
0 commit comments