935 lines
41 KiB
Python
935 lines
41 KiB
Python
from typing import Literal
|
|
|
|
import markupsafe
|
|
import requests
|
|
|
|
from odoo import api, fields, models, _
|
|
from odoo.addons.l10n_ro_edi_stock.models.l10n_ro_edi_stock_document import DOCUMENT_STATES
|
|
from odoo.addons.l10n_ro_edi_stock.models.etransport_api import ETransportAPI
|
|
from odoo.exceptions import UserError
|
|
|
|
OPERATION_TYPES = [
|
|
('10', "Intra-community purchase"),
|
|
('12', "Operations in lohn system (EU) - input"),
|
|
('14', "Stocks available to the customer (Call-off stock) - entry"),
|
|
('20', "Intra-Community delivery"),
|
|
('22', "Operations in lohn system (EU) - exit"),
|
|
('24', "Stocks available to the customer (Call-off stock) - exit"),
|
|
('30', "Transport on the national territory"),
|
|
('40', "Import"),
|
|
('50', "Export"),
|
|
('60', "Intra-community transaction - Entry for storage/formation of new transport"),
|
|
('70', "Intra-community transaction - Exit after storage/formation of new transport"),
|
|
]
|
|
|
|
OPERATION_SCOPES = [
|
|
('101', "Marketing"),
|
|
('201', "Output"),
|
|
('301', "Gratuities"),
|
|
('401', "Commercial equipment"),
|
|
('501', "Fixed assets"),
|
|
('601', "Own consumption"),
|
|
('703', "Delivery operations with installation"),
|
|
('704', "Transfer between managements"),
|
|
('705', "Goods made available to the customer"),
|
|
('801', "Financial/operational leasing"),
|
|
('802', "Goods under warranty"),
|
|
('901', "Exempt operations"),
|
|
('1001', "Investment in progress"),
|
|
('1101', "Donations, help"),
|
|
('9901', "Other"),
|
|
('9999', "Same with operation"),
|
|
]
|
|
|
|
OPERATION_TYPE_TO_ALLOWED_SCOPE_CODES = {
|
|
"10": ("101", "201", "301", "401", "501", "601", "703", "801", "802", "901", "1001", "1101", "9901"),
|
|
"20": ("101", "301", "703", "801", "802", "9901"),
|
|
"30": ("101", "704", "705", "9901"),
|
|
}
|
|
|
|
LOCATION_TYPES = [('location', "Location"), ('bcp', "Border Crossing Point"), ('customs', "Customs Office")]
|
|
|
|
LOCATION_TYPE_MAP = {
|
|
'start': {
|
|
'customs_code': '40',
|
|
'bcp_codes': ('10', '12', '14', '60'),
|
|
},
|
|
'end': {
|
|
'customs_code': '50',
|
|
'bcp_codes': ('10', '20', '22', '24', '70'),
|
|
}
|
|
}
|
|
|
|
BORDER_CROSSING_POINTS = [
|
|
('1', "Petea (HU)"),
|
|
('2', "Borș(HU)"),
|
|
('3', "Vărșand(HU)"),
|
|
('4', "Nădlac(HU)"),
|
|
('5', "Calafat (BG)"),
|
|
('6', "Bechet(BG)"),
|
|
('7', "Turnu Măgurele(BG)"),
|
|
('8', "Zimnicea(BG)"),
|
|
('9', "Giurgiu(BG)"),
|
|
('10', "Ostrov(BG)"),
|
|
('11', "Negru Vodă(BG)"),
|
|
('12', "Vama Veche(BG)"),
|
|
('13', "Călărași(BG)"),
|
|
('14', "Corabia(BG)"),
|
|
('15', "Oltenița(BG)"),
|
|
('16', "Carei (HU)"),
|
|
('17', "Cenad (HU)"),
|
|
('18', "Episcopia Bihor (HU)"),
|
|
('19', "Salonta (HU)"),
|
|
('20', "Săcuieni (HU)"),
|
|
('21', "Turnu (HU)"),
|
|
('22', "Urziceni (HU)"),
|
|
('23', "Valea lui Mihai (HU)"),
|
|
('24', "Vladimirescu (HU)"),
|
|
('25', "Porțile de Fier 1 (RS)"),
|
|
('26', "Naidăș(RS)"),
|
|
('27', "Stamora Moravița(RS)"),
|
|
('28', "Jimbolia(RS)"),
|
|
('29', "Halmeu (UA)"),
|
|
('30', "Stânca Costești (MD)"),
|
|
('31', "Sculeni(MD)"),
|
|
('32', "Albița(MD)"),
|
|
('33', "Oancea(MD)"),
|
|
('34', "Galați Giurgiulești(MD)"),
|
|
('35', "Constanța Sud Agigea"),
|
|
('36', "Siret (UA)"),
|
|
('37', "Nădlac 2 - A1 (HU)"),
|
|
('38', "Borș 2 - A3 (HU)"),
|
|
]
|
|
|
|
CUSTOMS_OFFICES = [
|
|
('12801', "BVI Alba Iulia (ROBV0300)"),
|
|
('22801', "BVI Arad (ROTM0200)"),
|
|
('22901', "BVF Arad Aeroport (ROTM0230)"),
|
|
('22902', "BVF Zona Liberă Curtici (ROTM2300)"),
|
|
('32801', "BVI Pitești (ROCR7000)"),
|
|
('42801', "BVI Bacău (ROIS0600)"),
|
|
('42901', "BVF Bacău Aeroport (ROIS0620)"),
|
|
('52801', "BVI Oradea (ROCJ6570)"),
|
|
('52901', "BVF Oradea Aeroport (ROCJ6580)"),
|
|
('62801', "BVI Bistriţa-Năsăud (ROCJ0400)"),
|
|
('72801', "BVI Botoşani (ROIS1600)"),
|
|
('72901', "BVF Stanca Costeşti (ROIS1610)"),
|
|
('72902', "BVF Rădăuţi Prut (ROIS1620)"),
|
|
('82801', "BVI Braşov (ROBV0900)"),
|
|
('92901', "BVF Zona Liberă Brăila (ROGL0710)"),
|
|
('92902', "BVF Brăila (ROGL0700)"),
|
|
('102801', "BVI Buzău (ROGL1500)"),
|
|
('112801', "BVI Reșița (ROTM7600)"),
|
|
('112901', "BVF Naidăș (ROTM6100)"),
|
|
('122801', "BVI Cluj Napoca (ROCJ1800)"),
|
|
('122901', "BVF Cluj Napoca Aero (ROCJ1810)"),
|
|
('132901', "BVF Constanţa Sud Agigea (ROCT1900)"),
|
|
('132902', "BVF Mihail Kogălniceanu (ROCT5100)"),
|
|
('132903', "BVF Mangalia (ROCT5400)"),
|
|
('132904', "BVF Constanţa Port (ROCT1970)"),
|
|
('142801', "BVI Sfântu Gheorghe (ROBV7820)"),
|
|
('152801', "BVI Târgoviște (ROBU8600)"),
|
|
('162801', "BVI Craiova (ROCR2100)"),
|
|
('162901', "BVF Craiova Aeroport (ROCR2110)"),
|
|
('162902', "BVF Bechet (ROCR1720)"),
|
|
('162903', "BVF Calafat (ROCR1700)"),
|
|
('172901', "BVF Zona Liberă Galaţi (ROGL3810)"),
|
|
('172902', "BVF Giurgiuleşti (ROGL3850)"),
|
|
('172903', "BVF Oancea (ROGL3610)"),
|
|
('172904', "BVF Galaţi (ROGL3800)"),
|
|
('182801', "BVI Târgu Jiu (ROCR8810)"),
|
|
('192801', "BVI Miercurea Ciuc (ROBV5600)"),
|
|
('202801', "BVI Deva (ROTM8100)"),
|
|
('212801', "BVI Slobozia (ROCT8220)"),
|
|
('222901', "BVF Iaşi Aero (ROIS4660)"),
|
|
('222902', "BVF Sculeni (ROIS4990)"),
|
|
('222903', "BVF Iaşi (ROIS4650)"),
|
|
('232801', "BVI Antrepozite/Ilfov (ROBU1200)"),
|
|
('232901', "BVF Otopeni Călători (ROBU1030)"),
|
|
('242801', "BVI Baia Mare (ROCJ0500)"),
|
|
('242901', "BVF Aero Baia Mare (ROCJ0510)"),
|
|
('242902', "BVF Sighet (ROCJ8000)"),
|
|
('252901', "BVF Orşova (ROCR7280)"),
|
|
('252902', "BVF Porţile De Fier I (ROCR7270)"),
|
|
('252903', "BVF Porţile De Fier II (ROCR7200)"),
|
|
('252904', "BVF Drobeta Turnu Severin (ROCR9000)"),
|
|
('262801', "BVI Târgu Mureş (ROBV8800)"),
|
|
('262901', "BVF Târgu Mureş Aeroport (ROBV8820)"),
|
|
('272801', "BVI Piatra Neamţ (ROIS7400)"),
|
|
('282801', "BVI Corabia (ROCR2000)"),
|
|
('282802', "BVI Olt (ROCR8210)"),
|
|
('292801', "BVI Ploiești (ROBU7100)"),
|
|
('302801', "BVI Satu-Mare (ROCJ7810)"),
|
|
('302901', "BVF Halmeu (ROCJ4310)"),
|
|
('302902', "BVF Aeroport Satu Mare (ROCJ7830)"),
|
|
('312801', "BVI Zalău (ROCJ9700)"),
|
|
('322801', "BVI Sibiu (ROBV7900)"),
|
|
('322901', "BVF Sibiu Aeroport (ROBV7910)"),
|
|
('332801', "BVI Suceava (ROIS8230)"),
|
|
('332901', "BVF Dorneşti (ROIS2700)"),
|
|
('332902', "BVF Siret (ROIS8200)"),
|
|
('332903', "BVF Suceava Aero (ROIS8250)"),
|
|
('332904', "BVF Vicovu De Sus (ROIS9620)"),
|
|
('342801', "BVI Alexandria (ROCR0310)"),
|
|
('342901', "BVF Turnu Măgurele (ROCR9100)"),
|
|
('342902', "BVF Zimnicea (ROCR5800)"),
|
|
('352802', "BVI Timişoara Bază (ROTM8720)"),
|
|
('352901', "BVF Jimbolia (ROTM5010)"),
|
|
('352902', "BVF Moraviţa (ROTM5510)"),
|
|
('352903', "BVF Timişoara Aeroport (ROTM8730)"),
|
|
('362901', "BVF Sulina (ROCT8300)"),
|
|
('362902', "BVF Aeroport Delta Dunării Tulcea (ROGL8910)"),
|
|
('362903', "BVF Tulcea (ROGL8900)"),
|
|
('362904', "BVF Isaccea (ROGL8920)"),
|
|
('372801', "BVI Vaslui (ROIS9610)"),
|
|
('372901', "BVF Fălciu (-)"),
|
|
('372902', "BVF Albiţa (ROIS0100)"),
|
|
('382801', "BVI Râmnicu Vâlcea (ROCR7700)"),
|
|
('392801', "BVI Focșani (ROGL3600)"),
|
|
('402801', "BVI Bucureşti Poştă (ROBU1380)"),
|
|
('402802', "BVI Târguri și Expoziții (ROBU1400)"),
|
|
('402901', "BVF Băneasa (ROBU1040)"),
|
|
('512801', "BVI Călăraşi (ROCT1710)"),
|
|
('522801', "BVI Giurgiu (ROBU3910)"),
|
|
('522901', "BVF Zona Liberă Giurgiu (ROBU3980)"),
|
|
]
|
|
|
|
STATE_CODES = {
|
|
'AB': '1',
|
|
'AR': '2',
|
|
'AG': '3',
|
|
'BC': '4',
|
|
'BH': '5',
|
|
'BN': '6',
|
|
'BT': '7',
|
|
'BV': '8',
|
|
'BR': '9',
|
|
'BZ': '10',
|
|
'CS': '11',
|
|
'CJ': '12',
|
|
'CT': '13',
|
|
'CV': '14',
|
|
'DB': '15',
|
|
'DJ': '16',
|
|
'GL': '17',
|
|
'GJ': '18',
|
|
'HR': '19',
|
|
'HD': '20',
|
|
'IL': '21',
|
|
'IS': '22',
|
|
'IF': '23',
|
|
'MM': '24',
|
|
'MH': '25',
|
|
'MS': '26',
|
|
'NT': '27',
|
|
'OT': '28',
|
|
'PH': '29',
|
|
'SM': '30',
|
|
'SJ': '31',
|
|
'SB': '32',
|
|
'SV': '33',
|
|
'TR': '34',
|
|
'TM': '35',
|
|
'TL': '36',
|
|
'VS': '37',
|
|
'VL': '38',
|
|
'VN': '39',
|
|
'B': '40',
|
|
'CL': '51',
|
|
'GR': '52',
|
|
}
|
|
|
|
|
|
class Picking(models.Model):
|
|
_inherit = 'stock.picking'
|
|
|
|
# Document fields
|
|
l10n_ro_edi_stock_document_ids = fields.One2many(comodel_name='l10n_ro_edi.document', inverse_name='picking_id')
|
|
l10n_ro_edi_stock_document_uit = fields.Char(compute='_compute_l10n_ro_edi_stock_current_document_uit', string="eTransport UIT")
|
|
l10n_ro_edi_stock_state = fields.Selection(
|
|
selection=DOCUMENT_STATES,
|
|
compute='_compute_l10n_ro_edi_stock_current_document_state',
|
|
string="eTransport Status",
|
|
store=True,
|
|
)
|
|
|
|
# Data fields
|
|
l10n_ro_edi_stock_operation_type = fields.Selection(selection=OPERATION_TYPES, string="eTransport Operation Type")
|
|
l10n_ro_edi_stock_available_operation_scopes = fields.Char(compute='_compute_l10n_ro_edi_stock_available_operation_scopes')
|
|
l10n_ro_edi_stock_operation_scope = fields.Selection(selection=OPERATION_SCOPES, string="Operation Scope")
|
|
|
|
l10n_ro_edi_stock_vehicle_number = fields.Char(string="Vehicle Number", size=20)
|
|
l10n_ro_edi_stock_trailer_1_number = fields.Char(string="Trailer 1 Number", size=20)
|
|
l10n_ro_edi_stock_trailer_2_number = fields.Char(string="Trailer 2 Number", size=20)
|
|
|
|
l10n_ro_edi_stock_available_start_loc_types = fields.Char(compute='_compute_l10n_ro_edi_stock_available_location_types')
|
|
l10n_ro_edi_stock_start_loc_type = fields.Selection(
|
|
selection=LOCATION_TYPES,
|
|
string="Start Location Type",
|
|
compute='_compute_l10n_ro_edi_stock_default_location_type',
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
|
|
l10n_ro_edi_stock_available_end_loc_types = fields.Char(compute='_compute_l10n_ro_edi_stock_available_location_types')
|
|
l10n_ro_edi_stock_end_loc_type = fields.Selection(
|
|
selection=LOCATION_TYPES,
|
|
string="End Location Type",
|
|
compute='_compute_l10n_ro_edi_stock_default_location_type',
|
|
store=True,
|
|
readonly=False,
|
|
)
|
|
|
|
l10n_ro_edi_stock_start_bcp = fields.Selection(selection=BORDER_CROSSING_POINTS, string="Start Border Crossing Point")
|
|
l10n_ro_edi_stock_start_customs_office = fields.Selection(selection=CUSTOMS_OFFICES, string="Start Customs Office")
|
|
l10n_ro_edi_stock_end_bcp = fields.Selection(selection=BORDER_CROSSING_POINTS, string="End Border Crossing Point")
|
|
l10n_ro_edi_stock_end_customs_office = fields.Selection(selection=CUSTOMS_OFFICES, string="End Customs Office")
|
|
|
|
l10n_ro_edi_stock_remarks = fields.Text(string="Remarks")
|
|
|
|
# View control fields
|
|
l10n_ro_edi_stock_enable = fields.Boolean(compute='_compute_l10n_ro_edi_stock_enable')
|
|
l10n_ro_edi_stock_enable_send = fields.Boolean(compute='_compute_l10n_ro_edi_stock_enable_send')
|
|
l10n_ro_edi_stock_enable_fetch = fields.Boolean(compute='_compute_l10n_ro_edi_stock_enable_fetch')
|
|
l10n_ro_edi_stock_enable_amend = fields.Boolean(compute='_compute_l10n_ro_edi_stock_enable_amend')
|
|
|
|
l10n_ro_edi_stock_fields_readonly = fields.Boolean(compute='_compute_l10n_ro_edi_stock_fields_readonly')
|
|
|
|
################################################################################
|
|
# Onchange Methods
|
|
################################################################################
|
|
|
|
@api.onchange('l10n_ro_edi_stock_operation_type')
|
|
def _l10n_ro_edi_stock_reset_variable_selection_fields(self):
|
|
self.l10n_ro_edi_stock_operation_scope = False
|
|
|
|
# the 'location' value is always valid, regardless of which operation type is chosen
|
|
self.l10n_ro_edi_stock_start_loc_type = 'location'
|
|
self.l10n_ro_edi_stock_end_loc_type = 'location'
|
|
|
|
################################################################################
|
|
# Compute Methods
|
|
################################################################################
|
|
|
|
@api.depends('company_id.account_fiscal_country_id.code')
|
|
def _compute_l10n_ro_edi_stock_default_location_type(self):
|
|
for picking in self:
|
|
if picking.company_id.account_fiscal_country_id.code == 'RO':
|
|
if not picking.l10n_ro_edi_stock_start_loc_type:
|
|
picking.l10n_ro_edi_stock_start_loc_type = 'location'
|
|
else:
|
|
picking.l10n_ro_edi_stock_start_loc_type = picking.l10n_ro_edi_stock_start_loc_type
|
|
|
|
if not picking.l10n_ro_edi_stock_end_loc_type:
|
|
picking.l10n_ro_edi_stock_end_loc_type = 'location'
|
|
else:
|
|
picking.l10n_ro_edi_stock_end_loc_type = picking.l10n_ro_edi_stock_end_loc_type
|
|
else:
|
|
picking.l10n_ro_edi_stock_start_loc_type = False
|
|
picking.l10n_ro_edi_stock_end_loc_type = False
|
|
|
|
@api.depends('l10n_ro_edi_stock_operation_type')
|
|
def _compute_l10n_ro_edi_stock_available_operation_scopes(self):
|
|
for picking in self:
|
|
if picking.l10n_ro_edi_stock_operation_type:
|
|
allowed_scopes = OPERATION_TYPE_TO_ALLOWED_SCOPE_CODES.get(picking.l10n_ro_edi_stock_operation_type, ("9999",))
|
|
else:
|
|
allowed_scopes = [c for c, _dummy in OPERATION_SCOPES]
|
|
|
|
picking.l10n_ro_edi_stock_available_operation_scopes = ','.join(allowed_scopes)
|
|
|
|
@api.depends('l10n_ro_edi_stock_operation_type')
|
|
def _compute_l10n_ro_edi_stock_available_location_types(self):
|
|
for picking in self:
|
|
picking.l10n_ro_edi_stock_available_start_loc_types = picking._l10n_ro_edi_stock_get_available_location_types(picking.l10n_ro_edi_stock_operation_type, 'start')
|
|
picking.l10n_ro_edi_stock_available_end_loc_types = picking._l10n_ro_edi_stock_get_available_location_types(picking.l10n_ro_edi_stock_operation_type, 'end')
|
|
|
|
@api.depends('l10n_ro_edi_stock_document_ids', 'company_id.account_fiscal_country_id.code')
|
|
def _compute_l10n_ro_edi_stock_current_document_state(self):
|
|
for picking in self:
|
|
if picking.company_id.account_fiscal_country_id.code == 'RO' and (document := picking._l10n_ro_edi_stock_get_current_document()):
|
|
picking.l10n_ro_edi_stock_state = document.state
|
|
else:
|
|
picking.l10n_ro_edi_stock_state = False
|
|
|
|
@api.depends('l10n_ro_edi_stock_document_ids', 'company_id.account_fiscal_country_id.code')
|
|
def _compute_l10n_ro_edi_stock_current_document_uit(self):
|
|
for picking in self:
|
|
if picking.company_id.account_fiscal_country_id.code == 'RO' and (document := picking._l10n_ro_edi_stock_get_current_document()):
|
|
picking.l10n_ro_edi_stock_document_uit = document.l10n_ro_edi_stock_uit
|
|
else:
|
|
picking.l10n_ro_edi_stock_document_uit = False
|
|
|
|
@api.depends('company_id.account_fiscal_country_id.code')
|
|
def _compute_l10n_ro_edi_stock_enable(self):
|
|
for picking in self:
|
|
picking.l10n_ro_edi_stock_enable = picking.company_id.account_fiscal_country_id.code == 'RO'
|
|
|
|
@api.depends('l10n_ro_edi_stock_enable', 'state', 'l10n_ro_edi_stock_state')
|
|
def _compute_l10n_ro_edi_stock_enable_send(self):
|
|
for picking in self:
|
|
picking.l10n_ro_edi_stock_enable_send = (
|
|
picking.l10n_ro_edi_stock_enable
|
|
and picking.state == 'done'
|
|
and picking.l10n_ro_edi_stock_state in (False, 'stock_sending_failed')
|
|
and not picking._l10n_ro_edi_stock_get_last_document('stock_validated')
|
|
)
|
|
|
|
@api.depends('company_id', 'state', 'l10n_ro_edi_stock_state')
|
|
def _compute_l10n_ro_edi_stock_enable_fetch(self):
|
|
for picking in self:
|
|
picking.l10n_ro_edi_stock_enable_fetch = picking.l10n_ro_edi_stock_enable and picking.l10n_ro_edi_stock_state == 'stock_sent'
|
|
|
|
@api.depends('l10n_ro_edi_stock_state')
|
|
def _compute_l10n_ro_edi_stock_enable_amend(self):
|
|
for picking in self:
|
|
picking.l10n_ro_edi_stock_enable_amend = picking.l10n_ro_edi_stock_enable and (
|
|
picking.l10n_ro_edi_stock_state == 'stock_validated'
|
|
or (
|
|
picking.l10n_ro_edi_stock_state == 'stock_sending_failed'
|
|
and picking._l10n_ro_edi_stock_get_last_document('stock_validated')
|
|
)
|
|
)
|
|
|
|
@api.depends('l10n_ro_edi_stock_state')
|
|
def _compute_l10n_ro_edi_stock_fields_readonly(self):
|
|
for picking in self:
|
|
picking.l10n_ro_edi_stock_fields_readonly = picking.l10n_ro_edi_stock_state == 'stock_sent'
|
|
|
|
################################################################################
|
|
# Validation methods
|
|
################################################################################
|
|
|
|
def button_validate(self):
|
|
# EXTENDS 'stock'
|
|
|
|
# Validate the carrier first because it cannot be changed after the super call
|
|
self._l10n_ro_edi_stock_validate_carrier()
|
|
|
|
return super().button_validate()
|
|
|
|
def _l10n_ro_edi_stock_validate_carrier(self):
|
|
for picking in self.filtered(self._l10n_ro_edi_stock_validate_carrier_filter):
|
|
# validate carrier
|
|
if not picking.carrier_id:
|
|
raise UserError(_("The picking %(picking_name)s is missing a delivery carrier.", picking_name=picking.name))
|
|
|
|
# validate carrier partner
|
|
if not picking.carrier_id.l10n_ro_edi_stock_partner_id:
|
|
raise UserError(_("The delivery carrier of %(picking_name)s is missing the partner field value.", picking_name=picking.name))
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_validate_carrier_filter(self, picking):
|
|
# To be overridden by stock.picking.batch
|
|
return picking.l10n_ro_edi_stock_enable
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_validate_data(self, data: dict):
|
|
errors = []
|
|
|
|
# API access token
|
|
if not data['company_id'].l10n_ro_edi_access_token:
|
|
errors.append(_('Romanian access token not found. Please generate or fill it in the settings.'))
|
|
|
|
# carrier partner fields
|
|
partner = data['transport_partner_id']
|
|
missing_carrier_partner_fields = []
|
|
|
|
if partner.country_id.code != 'RO':
|
|
errors.append(_("The delivery carrier partner has to be located in Romania."))
|
|
|
|
if not partner.vat:
|
|
missing_carrier_partner_fields.append(_("VAT"))
|
|
|
|
if not partner.city:
|
|
missing_carrier_partner_fields.append(_("City"))
|
|
|
|
if not partner.street:
|
|
missing_carrier_partner_fields.append(_("Street"))
|
|
|
|
if len(missing_carrier_partner_fields) == 1:
|
|
errors.append(_("The delivery carrier partner is missing the %(field_name)s field.", field_name=missing_carrier_partner_fields[0]))
|
|
elif len(missing_carrier_partner_fields) > 1:
|
|
errors.append(_("The delivery carrier partner is missing following fields: %(field_names)s", field_names=', '.join(missing_carrier_partner_fields)))
|
|
|
|
# operation type
|
|
if not data['l10n_ro_edi_stock_operation_type']:
|
|
errors.append(_("Operation type is missing."))
|
|
return errors # return prematurely because a lot of fields depend on the operation type
|
|
|
|
# operation scope
|
|
if not data['l10n_ro_edi_stock_operation_scope']:
|
|
errors.append(_("Operation scope is missing."))
|
|
|
|
# vehicle & trailer numbers
|
|
if not data['l10n_ro_edi_stock_vehicle_number']:
|
|
errors.append(_("Vehicle number is missing."))
|
|
|
|
# All filled-in vehicle and trailer numbers must be unique
|
|
license_plates = [num for num in (data['l10n_ro_edi_stock_vehicle_number'], data['l10n_ro_edi_stock_trailer_1_number'], data['l10n_ro_edi_stock_trailer_2_number']) if num]
|
|
if len(license_plates) != len(set(license_plates)):
|
|
errors.append(_("Vehicle number and trailer number fields must be unique."))
|
|
|
|
# rate codes
|
|
if 'intrastat_code_id' in self.env['product.product']._fields and data['l10n_ro_edi_stock_operation_type'] not in ('60', '70'):
|
|
product_without_code_names = {move_line.product_id.name
|
|
for move in data['stock_move_ids']
|
|
for move_line in move.move_line_ids
|
|
if not move_line.product_id.intrastat_code_id.code}
|
|
|
|
if product_without_code_names:
|
|
if len(product_without_code_names) == 1:
|
|
(product_name,) = product_without_code_names
|
|
errors.append(_("Product %(name)s is missing the intrastat code value.", name=product_name))
|
|
else:
|
|
errors.append(_("Products %(names)s are missing the intrastat code value.", names=", ".join(product_without_code_names)))
|
|
|
|
# Location types
|
|
if not data['l10n_ro_edi_stock_start_loc_type']:
|
|
if not data['l10n_ro_edi_stock_end_loc_type']:
|
|
errors.append(_("Both 'End' and 'Start Location Type' are missing"))
|
|
else:
|
|
errors.append(_("'Start Location Type' is missing"))
|
|
|
|
return errors # return prematurely because all the start location fields depend on this field
|
|
|
|
if not data['l10n_ro_edi_stock_end_loc_type']:
|
|
errors.append(_("'End Location Type' is missing"))
|
|
return errors # return prematurely because all the end location fields depend on this field
|
|
|
|
# Location fields
|
|
for location in ('start', 'end'):
|
|
loc_value = data[f'l10n_ro_edi_stock_{location}_loc_type']
|
|
loc_group = _("'Start Location'") if location == 'start' else _("'End Location'")
|
|
|
|
if loc_value == 'bcp' and not data[f'l10n_ro_edi_stock_{location}_bcp']:
|
|
errors.append(_("The border crossing point is missing under %(location_group)s", location_group=loc_group))
|
|
elif loc_value == 'customs' and not data[f'l10n_ro_edi_stock_{location}_customs_office']:
|
|
errors.append(_("The customs office is missing under %(location_group)s", location_group=loc_group))
|
|
elif loc_value == 'location':
|
|
match data['picking_type_id'].code:
|
|
case 'outgoing':
|
|
partner = data['picking_type_id'].warehouse_id.partner_id if location == 'start' else data['partner_id']
|
|
case 'incoming':
|
|
partner = data['picking_type_id'].warehouse_id.partner_id if location == 'end' else data['partner_id']
|
|
case _other:
|
|
errors.append(_("Invalid picking type %(type_code)s", type_code=_other))
|
|
continue
|
|
|
|
missing_field_names = []
|
|
if not partner.state_id:
|
|
missing_field_names.append(_("State"))
|
|
if not partner.city:
|
|
missing_field_names.append(_("City"))
|
|
if not partner.street:
|
|
missing_field_names.append(_("Street"))
|
|
if not partner.zip:
|
|
missing_field_names.append(_("Postal Code"))
|
|
|
|
if len(missing_field_names) == 1:
|
|
errors.append(_("%(location_group)s is missing the %(field_name)s field.", location_group=loc_group, field_name=missing_field_names[0]))
|
|
elif len(missing_field_names) > 1:
|
|
errors.append(_("%(location_group)s is missing following fields: %(field_names)s", location_group=loc_group, field_names=missing_field_names))
|
|
|
|
return errors
|
|
|
|
def _l10n_ro_edi_stock_validate_fetch_data(self, errors=None):
|
|
if errors is None:
|
|
errors = []
|
|
self.ensure_one()
|
|
|
|
if not self.company_id.l10n_ro_edi_access_token:
|
|
errors.append(_('Romanian access token not found. Please generate or fill it in the settings.'))
|
|
return errors
|
|
|
|
match self.l10n_ro_edi_stock_state:
|
|
case 'stock_sending_failed':
|
|
if not self._l10n_ro_edi_stock_get_last_document('stock_validated'):
|
|
errors.append(_("This document has not been successfully sent yet because it contains errors."))
|
|
else:
|
|
errors.append(_("This document has not been corrected yet because it contains errors."))
|
|
case 'stock_validated':
|
|
errors.append(_("This document has already been successfully sent to anaf."))
|
|
|
|
return errors
|
|
|
|
################################################################################
|
|
# Actions
|
|
################################################################################
|
|
|
|
def action_l10n_ro_edi_stock_send_etransport(self):
|
|
self.ensure_one()
|
|
|
|
send_type = self.env.context.get('l10n_ro_edi_stock_send_type', 'send')
|
|
self._l10n_ro_edi_stock_send_etransport_document(send_type=send_type)
|
|
|
|
def action_l10n_ro_edi_stock_fetch_status(self):
|
|
self._l10n_ro_edi_stock_fetch_document_status()
|
|
|
|
################################################################################
|
|
# Document Helpers
|
|
################################################################################
|
|
|
|
def _l10n_ro_edi_stock_get_current_document(self):
|
|
"""
|
|
Returns the most recently created document in l10n_ro_edi_stock_document_ids
|
|
"""
|
|
self.ensure_one()
|
|
return self.l10n_ro_edi_stock_document_ids.sorted()[0] if self.l10n_ro_edi_stock_document_ids else None
|
|
|
|
def _l10n_ro_edi_stock_get_all_documents(self, states):
|
|
"""
|
|
Returns filtered documents by state
|
|
"""
|
|
self.ensure_one()
|
|
|
|
if isinstance(states, str):
|
|
states = [states]
|
|
|
|
return self.l10n_ro_edi_stock_document_ids.filtered(lambda doc: doc.state in states)
|
|
|
|
def _l10n_ro_edi_stock_get_last_document(self, state):
|
|
"""
|
|
Returns the most recently created document with the given state
|
|
"""
|
|
self.ensure_one()
|
|
documents_in_state = self.l10n_ro_edi_stock_document_ids.filtered(lambda doc: doc.state == state).sorted()
|
|
|
|
return documents_in_state and documents_in_state[0]
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_create_attachment(self, values: dict):
|
|
data = {
|
|
'name': f"etransport_{values['name'].replace('/', '_')}.xml",
|
|
'res_model': 'l10n_ro_edi.document',
|
|
'res_id': values['res_id'],
|
|
'raw': values['raw'],
|
|
'type': 'binary',
|
|
'mimetype': 'application/xml',
|
|
}
|
|
|
|
return self.env['ir.attachment'].sudo().create(data)
|
|
|
|
def _l10n_ro_edi_stock_create_document_stock_sent(self, values: dict[str, object]):
|
|
self.ensure_one()
|
|
document = self.env['l10n_ro_edi.document'].create({
|
|
'picking_id': self.id,
|
|
'state': 'stock_sent',
|
|
'l10n_ro_edi_stock_load_id': values['l10n_ro_edi_stock_load_id'],
|
|
'l10n_ro_edi_stock_uit': values['l10n_ro_edi_stock_uit'],
|
|
})
|
|
|
|
document.attachment_id = self._l10n_ro_edi_stock_create_attachment({
|
|
'name': self.name,
|
|
'res_id': document.id,
|
|
'raw': values['raw_xml'],
|
|
})
|
|
|
|
return document
|
|
|
|
def _l10n_ro_edi_stock_create_document_stock_sending_failed(self, values: dict[str, object]):
|
|
self.ensure_one()
|
|
document = self.env['l10n_ro_edi.document'].create({
|
|
'picking_id': self.id,
|
|
'state': 'stock_sending_failed',
|
|
'message': values['message'],
|
|
'l10n_ro_edi_stock_load_id': values.get('l10n_ro_edi_stock_load_id'),
|
|
'l10n_ro_edi_stock_uit': values.get('l10n_ro_edi_stock_uit'),
|
|
})
|
|
|
|
if 'raw_xml' in values:
|
|
# when an error is thrown during data validation there will be no 'raw_xml'
|
|
document.attachment_id = self._l10n_ro_edi_stock_create_attachment({
|
|
'name': self.name,
|
|
'res_id': document.id,
|
|
'raw': values['raw_xml'],
|
|
})
|
|
|
|
return document
|
|
|
|
def _l10n_ro_edi_stock_create_document_stock_validated(self, values: dict[str, object]):
|
|
self.ensure_one()
|
|
document = self.env['l10n_ro_edi.document'].create({
|
|
'picking_id': self.id,
|
|
'state': 'stock_validated',
|
|
'l10n_ro_edi_stock_load_id': values['l10n_ro_edi_stock_load_id'],
|
|
'l10n_ro_edi_stock_uit': values['l10n_ro_edi_stock_uit'],
|
|
})
|
|
|
|
document.attachment_id = self._l10n_ro_edi_stock_create_attachment({
|
|
'name': self.name,
|
|
'res_id': document.id,
|
|
'raw': values['raw_xml'],
|
|
})
|
|
|
|
return document
|
|
|
|
################################################################################
|
|
# Send Logic
|
|
################################################################################
|
|
|
|
def _l10n_ro_edi_stock_send_etransport_document(self, send_type: str):
|
|
"""
|
|
Send the eTransport document to anaf
|
|
:param send_type: 'send' (initial sending of document) | 'amend' (correct the already sent document)
|
|
"""
|
|
self.ensure_one()
|
|
|
|
data = {
|
|
'partner_id': self.partner_id,
|
|
'transport_partner_id': self.carrier_id.l10n_ro_edi_stock_partner_id,
|
|
'company_id': self.company_id,
|
|
'scheduled_date': self.scheduled_date,
|
|
'name': self.name,
|
|
'send_type': send_type,
|
|
'l10n_ro_edi_stock_operation_type': self.l10n_ro_edi_stock_operation_type,
|
|
'l10n_ro_edi_stock_operation_scope': self.l10n_ro_edi_stock_operation_scope,
|
|
'stock_move_ids': self.move_ids,
|
|
'l10n_ro_edi_stock_vehicle_number': self.l10n_ro_edi_stock_vehicle_number,
|
|
'l10n_ro_edi_stock_trailer_1_number': self.l10n_ro_edi_stock_trailer_1_number,
|
|
'l10n_ro_edi_stock_trailer_2_number': self.l10n_ro_edi_stock_trailer_2_number,
|
|
'l10n_ro_edi_stock_start_loc_type': self.l10n_ro_edi_stock_start_loc_type,
|
|
'l10n_ro_edi_stock_end_loc_type': self.l10n_ro_edi_stock_end_loc_type,
|
|
'l10n_ro_edi_stock_remarks': self.l10n_ro_edi_stock_remarks,
|
|
'picking_type_id': self.picking_type_id,
|
|
'l10n_ro_edi_stock_start_bcp': self.l10n_ro_edi_stock_start_bcp,
|
|
'l10n_ro_edi_stock_end_bcp': self.l10n_ro_edi_stock_end_bcp,
|
|
'l10n_ro_edi_stock_start_customs_office': self.l10n_ro_edi_stock_start_customs_office,
|
|
'l10n_ro_edi_stock_end_customs_office': self.l10n_ro_edi_stock_end_customs_office,
|
|
'l10n_ro_edi_stock_document_uit': self.l10n_ro_edi_stock_document_uit,
|
|
}
|
|
|
|
if errors := self._l10n_ro_edi_stock_validate_data(data=data):
|
|
document_values = {'message': '\n'.join(errors)}
|
|
|
|
if send_type == 'amend':
|
|
last_sent_document = self._l10n_ro_edi_stock_get_last_document('stock_validated')
|
|
document_values |= {
|
|
'l10n_ro_edi_stock_load_id': last_sent_document.l10n_ro_edi_stock_load_id,
|
|
'l10n_ro_edi_stock_uit': last_sent_document.l10n_ro_edi_stock_uit,
|
|
'raw_xml': last_sent_document.attachment_id.raw,
|
|
}
|
|
|
|
self._l10n_ro_edi_stock_create_document_stock_sending_failed(document_values)
|
|
return
|
|
|
|
raw_xml = markupsafe.Markup("<?xml version='1.0' encoding='UTF-8'?>\n") + self.env['ir.qweb']._render(
|
|
'l10n_ro_edi_stock.l10n_ro_template_etransport',
|
|
values=self._l10n_ro_edi_stock_get_template_data(data=data),
|
|
)
|
|
|
|
result = ETransportAPI().upload_data(company_id=self.company_id, data=raw_xml)
|
|
|
|
if 'error' in result:
|
|
document_values = {'message': result['error'], 'raw_xml': raw_xml}
|
|
|
|
if send_type == 'amend':
|
|
last_sent_document = self._l10n_ro_edi_stock_get_last_document('stock_validated')
|
|
document_values |= {
|
|
'l10n_ro_edi_stock_load_id': last_sent_document.l10n_ro_edi_stock_load_id,
|
|
'l10n_ro_edi_stock_uit': last_sent_document.l10n_ro_edi_stock_uit,
|
|
}
|
|
|
|
self._l10n_ro_edi_stock_create_document_stock_sending_failed(document_values)
|
|
else:
|
|
self._l10n_ro_edi_stock_get_all_documents({'stock_sending_failed', 'stock_sent'}).unlink()
|
|
|
|
content = result['content']
|
|
|
|
if send_type == 'send':
|
|
uit = content['UIT']
|
|
else:
|
|
last_validated = self._l10n_ro_edi_stock_get_last_document('stock_validated')
|
|
uit = last_validated.l10n_ro_edi_stock_uit
|
|
|
|
self._l10n_ro_edi_stock_create_document_stock_sent({
|
|
'l10n_ro_edi_stock_load_id': content['index_incarcare'],
|
|
'l10n_ro_edi_stock_uit': uit,
|
|
'raw_xml': raw_xml,
|
|
})
|
|
|
|
def _l10n_ro_edi_stock_fetch_document_status(self):
|
|
session = requests.Session()
|
|
documents_to_delete = self.env['l10n_ro_edi.document']
|
|
to_fetch = self.filtered(lambda p: p.l10n_ro_edi_stock_state == 'stock_sent')
|
|
|
|
for picking in to_fetch:
|
|
current_sending_document = picking.l10n_ro_edi_stock_document_ids.filtered(lambda doc: doc.state == 'stock_sent')[0]
|
|
|
|
if errors := picking._l10n_ro_edi_stock_validate_fetch_data():
|
|
picking._l10n_ro_edi_stock_create_document_stock_sending_failed({
|
|
'message': '\n'.join(errors),
|
|
'l10n_ro_edi_stock_load_id': current_sending_document.l10n_ro_edi_stock_load_id,
|
|
'l10n_ro_edi_stock_uit': current_sending_document.l10n_ro_edi_stock_uit,
|
|
'raw_xml': current_sending_document.attachment_id.raw,
|
|
})
|
|
continue
|
|
|
|
result = ETransportAPI().get_status(
|
|
company_id=picking.company_id,
|
|
document_load_id=current_sending_document.l10n_ro_edi_stock_load_id,
|
|
session=session,
|
|
)
|
|
|
|
if 'error' in result:
|
|
picking._l10n_ro_edi_stock_create_document_stock_sending_failed({
|
|
'message': result['error'],
|
|
'l10n_ro_edi_stock_load_id': current_sending_document.l10n_ro_edi_stock_load_id,
|
|
'l10n_ro_edi_stock_uit': current_sending_document.l10n_ro_edi_stock_uit,
|
|
'raw_xml': current_sending_document.attachment_id.raw,
|
|
})
|
|
else:
|
|
documents_to_delete |= picking._l10n_ro_edi_stock_get_all_documents(('stock_sent', 'stock_sending_failed'))
|
|
new_document_data = {
|
|
'l10n_ro_edi_stock_load_id': current_sending_document.l10n_ro_edi_stock_load_id,
|
|
'l10n_ro_edi_stock_uit': current_sending_document.l10n_ro_edi_stock_uit,
|
|
'raw_xml': current_sending_document.attachment_id.raw,
|
|
}
|
|
match state := result['content']['stare']:
|
|
case 'ok':
|
|
picking._l10n_ro_edi_stock_create_document_stock_validated(new_document_data)
|
|
case 'in prelucrare':
|
|
# Document is still being validated
|
|
picking._l10n_ro_edi_stock_create_document_stock_sent(new_document_data)
|
|
case 'XML cu erori nepreluat de sistem':
|
|
new_document_data['message'] = _("XML contains errors.")
|
|
picking._l10n_ro_edi_stock_create_document_stock_sending_failed(new_document_data)
|
|
case _:
|
|
picking._l10n_ro_edi_stock_report_unhandled_document_state(state)
|
|
|
|
documents_to_delete.unlink()
|
|
|
|
################################################################################
|
|
# Template helpers
|
|
################################################################################
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_get_template_data(self, data: dict):
|
|
"""
|
|
Returns the data necessary to render the eTransport template
|
|
"""
|
|
commercial_partner = data['partner_id'].commercial_partner_id
|
|
transport_partner = data['transport_partner_id']
|
|
company_id = data['company_id']
|
|
scheduled_date = data['scheduled_date'].date()
|
|
name = data['name']
|
|
commercial_partner_code = None
|
|
|
|
if commercial_partner.vat:
|
|
commercial_partner_code = self._l10n_ro_edi_stock_get_cod(commercial_partner)
|
|
elif self.l10n_ro_edi_stock_operation_type == '30':
|
|
commercial_partner_code = 'PF'
|
|
|
|
template_data = {
|
|
'send_type': data['send_type'],
|
|
'codDeclarant': self._l10n_ro_edi_stock_get_cod(company_id),
|
|
'refDeclarant': name,
|
|
'notificare': {
|
|
'codTipOperatiune': data['l10n_ro_edi_stock_operation_type'],
|
|
'bunuriTransportate': [
|
|
{
|
|
'codScopOperatiune': data['l10n_ro_edi_stock_operation_scope'],
|
|
'codTarifar': (product.intrastat_code_id.code if 'intrastat_code_id' in product._fields else None) or '00000000',
|
|
'denumireMarfa': product.name,
|
|
'cantitate': move.product_qty,
|
|
'codUnitateMasura': move.product_uom._get_unece_code(),
|
|
'greutateNeta': move.weight,
|
|
'greutateBruta': self._l10n_ro_edi_stock_get_gross_weight(move),
|
|
'valoareLeiFaraTva': product.list_price,
|
|
}
|
|
for move in data['stock_move_ids'] for product in move.product_id
|
|
],
|
|
'partenerComercial': {
|
|
'codTara': commercial_partner.country_code,
|
|
'denumire': commercial_partner.name,
|
|
'cod': commercial_partner_code,
|
|
},
|
|
'dateTransport': {
|
|
'nrVehicul': data['l10n_ro_edi_stock_vehicle_number'].upper(),
|
|
'nrRemorca1': data['l10n_ro_edi_stock_trailer_1_number'].upper() if data['l10n_ro_edi_stock_trailer_1_number'] else None,
|
|
'nrRemorca2': data['l10n_ro_edi_stock_trailer_2_number'].upper() if data['l10n_ro_edi_stock_trailer_2_number'] else None,
|
|
'codTaraOrgTransport': transport_partner.country_code,
|
|
'codOrgTransport': self._l10n_ro_edi_stock_get_cod(transport_partner),
|
|
'denumireOrgTransport': transport_partner.name,
|
|
'dataTransport': scheduled_date,
|
|
},
|
|
'locStartTraseuRutier': {
|
|
'location_type': data['l10n_ro_edi_stock_start_loc_type'],
|
|
},
|
|
'locFinalTraseuRutier': {
|
|
'location_type': data['l10n_ro_edi_stock_end_loc_type'],
|
|
},
|
|
'documenteTransport': {
|
|
'tipDocument': "30",
|
|
'dataDocument': scheduled_date,
|
|
'numarDocument': name,
|
|
'observatii': data['l10n_ro_edi_stock_remarks'],
|
|
}
|
|
},
|
|
}
|
|
|
|
if data['send_type'] == 'amend':
|
|
template_data['notificare']['uit'] = data['l10n_ro_edi_stock_document_uit']
|
|
|
|
for loc in ('start', 'end'):
|
|
key = 'locStartTraseuRutier' if loc == 'start' else 'locFinalTraseuRutier'
|
|
|
|
match template_data['notificare'][key]['location_type']:
|
|
case 'location':
|
|
match data['picking_type_id'].code:
|
|
case 'outgoing':
|
|
partner = data['picking_type_id'].warehouse_id.partner_id if loc == 'start' else data['partner_id']
|
|
case 'incoming':
|
|
partner = data['picking_type_id'].warehouse_id.partner_id if loc == 'end' else data['partner_id']
|
|
|
|
template_data['notificare'][key]['locatie'] = {
|
|
'codJudet': STATE_CODES[partner.state_id.code],
|
|
'denumireLocalitate': partner.city,
|
|
'denumireStrada': partner.street,
|
|
'codPostal': partner.zip,
|
|
'alteInfo': partner.street2,
|
|
}
|
|
case 'bcp':
|
|
template_data['notificare'][key]['codPtf'] = data[f'l10n_ro_edi_stock_{loc}_bcp']
|
|
case 'customs':
|
|
template_data['notificare'][key]['codBirouVamal'] = data[f'l10n_ro_edi_stock_{loc}_customs_office']
|
|
|
|
return {'data': template_data}
|
|
|
|
################################################################################
|
|
# Misc helpers
|
|
################################################################################
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_get_available_location_types(self, operation_type, location: Literal['start', 'end']) -> str:
|
|
"""
|
|
:return comma separated list of available location types for the start or end location based on the operation type
|
|
"""
|
|
if operation_type == LOCATION_TYPE_MAP[location]['customs_code']:
|
|
return 'location,bcp,customs'
|
|
elif operation_type in LOCATION_TYPE_MAP[location]['bcp_codes']:
|
|
return 'location,bcp'
|
|
else:
|
|
return 'location'
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_get_cod(self, record):
|
|
"""
|
|
:return the records vat in the format required by anaf
|
|
"""
|
|
return record.vat.upper().replace('RO', '')
|
|
|
|
@api.model
|
|
def _l10n_ro_edi_stock_get_gross_weight(self, move):
|
|
"""
|
|
:return the gross weight of a stock.move
|
|
"""
|
|
return move.weight + sum(line.result_package_id.shipping_weight for line in move.move_line_ids if line.result_package_id)
|
|
|
|
def _l10n_ro_edi_stock_report_unhandled_document_state(self, state: str):
|
|
"""
|
|
Reports an unknown document state from anaf to the user in the chatter
|
|
"""
|
|
self.ensure_one()
|
|
self.message_post(body=_("Unhandled eTransport document state: %(state)s", state=state))
|