Add ShipmentCSVProductMixin and stock.csv_import model.

This commit refs #12215
This commit is contained in:
jmpardo98 2020-04-01 14:53:25 +00:00 committed by Sergio Morillo
parent 11e4e33c7f
commit 7f9156c36b
17 changed files with 495 additions and 13 deletions

View File

@ -10,9 +10,15 @@ def register():
Pool.register(
configuration.Configuration,
stock.CsvImportStart,
stock.StockCsvImport,
shipment.ShipmentOutReturn,
shipment.ShipmentIn,
module='stock_csv_import', type_='model')
Pool.register(
stock.CsvImport,
module='stock_csv_import', type_='wizard')
Pool.register(
shipment.ShipmentOutReturnProduct,
shipment.ShipmentInProduct,
module='stock_csv_import', type_='model',
depends=['product_cross_reference'])

View File

@ -46,6 +46,30 @@ msgctxt "field:stock.shipment.csv_import.start,delimiter:"
msgid "Delimiter"
msgstr "Delimitador"
msgctxt "field:stock.shipment.csv_import.start,store_file:"
msgid "Store File"
msgstr "Guardar fichero"
msgctxt "field:stock.shipment.csv_import.start,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:stock.csv_import,name:"
msgid "Name"
msgstr "Nombre"
msgctxt "field:stock.csv_import,model:"
msgid "Model"
msgstr "Modelo"
msgctxt "field:stock.csv_import,date:"
msgid "Date"
msgstr "Fecha"
msgctxt "field:stock.csv_import,file:"
msgid "File"
msgstr "Fichero"
msgctxt "selection:stock.shipment.csv_import.start,shipment_type:"
msgid "In"
msgstr "Proveedor"
@ -68,4 +92,16 @@ msgstr "Importar CSV albarán proveedor"
msgctxt "model:ir.ui.menu,name:menu_wizard_shipment_in_csv_import"
msgid "Import CSV Shipment In"
msgstr "Importar CSV proveedor"
msgstr "Importar CSV proveedor"
msgctxt "model:ir.ui.menu,name:menu_stock_csv_import"
msgid "CSV Import"
msgstr "Importaciones CSV"
msgctxt "model:ir.action,name:act_stock_csv_import"
msgid "CSV Import"
msgstr "Importaciones CSV"
msgctxt "view:stock.csv_import:"
msgid "Time"
msgstr "Hora"

View File

@ -45,7 +45,13 @@ else:
branch = series
dependency_links = {
'product_cross_reference':
'git+https://gitlab.com/datalifeit/'
'trytond-product_cross_reference@%(branch)s'
'#egg=datalife_product_cross_reference-%(series)s' % {
'branch': branch,
'series': series,
}
}
requires = []
@ -58,7 +64,9 @@ for dep in info.get('depends', []):
requires.append(req)
requires.append(get_require_version('trytond'))
tests_require = [get_require_version('proteus')]
tests_require = [
get_require_version('proteus'),
'datalife_product_cross_reference']
dependency_links = list(dependency_links.values())
if minor_version % 2:

View File

@ -1,9 +1,10 @@
# The COPYRIGHT file at the top level of this repository contains the full
# copyright notices and license terms.
from trytond.pool import PoolMeta
from .stock import ShipmentCSVMixin
from .stock import ShipmentCSVMixin, ShipmentCSVProductMixin
__all__ = ['ShipmentOutReturn', 'ShipmentIn']
__all__ = ['ShipmentOutReturn', 'ShipmentIn', 'ShipmentInProduct',
'ShipmentOutReturnProduct']
class ShipmentOutReturn(ShipmentCSVMixin, metaclass=PoolMeta):
@ -52,3 +53,49 @@ class ShipmentIn(ShipmentCSVMixin, metaclass=PoolMeta):
def _set_csv_move_locations(self, move):
move.from_location = self.on_change_with_supplier_location()
move.to_location = self.on_change_with_warehouse_input()
class ShipmentOutReturnProduct(ShipmentCSVProductMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.out.return'
@classmethod
def _get_cross_reference_domain(cls, data):
domain = super()._get_cross_reference_domain(data)
subdomain = ['OR',
[
('party', '=', data['customer'].id),
('address', '=', None)],
[
('party', '=', None),
('address', '=', None)]
]
if ('delivery_address' in data.keys() and data['delivery_address']):
subdomain.append([
('party', '=', data['customer'].id),
('address', '=', data['delivery_address'].id)
])
domain.append(subdomain)
return domain
class ShipmentInProduct(ShipmentCSVProductMixin, metaclass=PoolMeta):
__name__ = 'stock.shipment.in'
@classmethod
def _get_cross_reference_domain(cls, data):
domain = super()._get_cross_reference_domain(data)
subdomain = ['OR',
[
('party', '=', data['supplier'].id),
('address', '=', None)],
[
('party', '=', None),
('address', '=', None)]
]
if ('contact_address' in data.keys() and data['contact_address']):
subdomain.append([
('party', '=', data['supplier'].id),
('address', '=', data['contact_address'].id)
])
domain.append(subdomain)
return domain

125
stock.py
View File

@ -6,7 +6,7 @@ import tempfile
from datetime import datetime
from collections import OrderedDict
from trytond.pool import Pool
from trytond.model import fields, ModelView
from trytond.model import fields, ModelView, ModelSQL
from trytond.pyson import Eval, Not, Bool
from trytond.transaction import Transaction
from trytond.wizard import Wizard, StateView, StateAction, Button
@ -17,7 +17,17 @@ from trytond.config import config
from os import path
from urllib.parse import urlparse
__all__ = ['ShipmentCSVMixin', 'CsvImport', 'CsvImportStart']
__all__ = ['ShipmentCSVMixin', 'CsvImport', 'CsvImportStart', 'StockCsvImport',
'ShipmentCSVProductMixin']
if config.getboolean('stock_csv_import', 'filestore', default=True):
file_id = 'file_id'
store_prefix = config.get('stock_csv_import', 'store_prefix',
default=None)
else:
file_id = None
store_prefix = None
class ShipmentCSVMixin(object):
@ -40,7 +50,7 @@ class ShipmentCSVMixin(object):
@classmethod
def _get_csv_field_value(cls, Model, field_name, field_value,
data=None, row=[]):
data=None, row=[], user_error=True):
pool = Pool()
splitted_field = []
if '.' in field_name:
@ -58,7 +68,7 @@ class ShipmentCSVMixin(object):
else:
domain.append(('rec_name', '=', field_value))
value = RelModel.search(domain)
if field_value and not value:
if user_error and field_value and not value:
cls.raise_user_error('csv_relation_not_found', (
Model.fields_get(fields_names=[fieldname]
)[fieldname]['string'],
@ -86,13 +96,17 @@ class ShipmentCSVMixin(object):
return headers or []
@classmethod
def import_csv(cls, csv_file, csv_delimiter=','):
def import_csv(cls, csv_file, csv_delimiter=',', name=None,
store_file=False):
pool = Pool()
Company = pool.get('company.company')
Move = pool.get('stock.move')
CsvImport = pool.get('stock.csv_import')
Model = pool.get('ir.model')
old_shipment = None
to_del = []
shipments = []
file = csv_file
config_header = cls._get_csv_headers()
@ -161,7 +175,7 @@ class ShipmentCSVMixin(object):
effective_date=shipment.effective_date)
for move_key, move_value in move_values.items():
_value = cls._get_csv_field_value(
Move, move_key, move_value)
Move, move_key, move_value, data=v)
move_field = move_key.split('.')[0]
setattr(move, move_field, _value)
shipment._set_csv_move_locations(move)
@ -177,6 +191,13 @@ class ShipmentCSVMixin(object):
shipment.incoming_moves = list(
shipment.incoming_moves) + moves
if store_file:
model, = Model.search([('model', '=', cls.__name__)])
csv_import = CsvImport(
name=name,
model=model,
file=file)
csv_import.save()
if shipments:
cls.save(shipments)
if to_del:
@ -260,6 +281,60 @@ class ShipmentCSVMixin(object):
os.path.join(folder_path, 'backup', filename))
class ShipmentCSVProductMixin(object):
@classmethod
def _get_cross_reference_model(cls, relation):
pool = Pool()
relation = relation.split('.')[0] + '.cross_reference'
return pool.get(relation)
@classmethod
def _get_cross_reference_domain(cls, data):
return []
@classmethod
def _get_csv_field_value(cls, Model, field_name, field_value,
data=None, row=[], user_error=True):
if '.' in field_name:
splitted_field = field_name.split('.')
fieldname = splitted_field[0]
else:
fieldname = field_name
if fieldname == 'product':
user_error = False
value = super()._get_csv_field_value(Model, field_name, field_value,
data=data, row=row, user_error=user_error)
if isinstance(Model._fields[fieldname], fields.Many2One):
field = Model._fields[fieldname]
relation = field.get_target().__name__
if relation == 'product.product' and not value:
if not splitted_field or (splitted_field and
splitted_field[1] in ('name', 'code')):
CrossReference = cls._get_cross_reference_model(relation)
domain = cls._get_cross_reference_domain(data)
if splitted_field:
domain.append((splitted_field[1], '=', field_value))
else:
domain.append(('rec_name', '=', field_value))
cross_reference = CrossReference.search(domain,
order=[
('party', 'ASC NULLS LAST'),
('address', 'ASC NULLS LAST')])
if not cross_reference:
cls.raise_user_error('csv_relation_not_found', (
Model.fields_get(fields_names=[fieldname]
)[fieldname]['string'],
field_value))
return (cross_reference[0].product
if cross_reference else None)
return value
class CsvImport(Wizard):
"""Shipment CSV Import"""
__name__ = 'stock.shipment.csv_import'
@ -297,12 +372,21 @@ class CsvImport(Wizard):
Shipment = pool.get(self.start.shipment_type)
csv_file = self.start.csv_file
return Shipment.import_csv(csv_file,
csv_delimiter=str(self.start.delimiter))
csv_delimiter=str(self.start.delimiter),
name=self.start.name, store_file=self.start.store_file)
def do_import_(self, action):
pool = Pool()
ModelData = pool.get('ir.model.data')
Action = pool.get('ir.action')
shipments = self._do_import()
action_id = Action.get_action_id(ModelData.get_id('stock',
'act_%s_form' % self.start.shipment_type[6:].replace('.', '_')))
action = Action(action_id)
data = {'res_id': [i.id for i in shipments] or []}
return action, data
return Action.get_action_values(action.type, [action.id])[0], data
def _get_model(self):
pass
@ -324,7 +408,32 @@ class CsvImportStart(ModelView):
delimiter = fields.Selection([
(',', ','),
(';', ';')], 'Delimiter', required=True)
store_file = fields.Boolean('Store File')
name = fields.Char('Name',
states={
'required': Bool(Eval('store_file'))},
depends=['store_file'])
@staticmethod
def default_delimiter():
return ','
@staticmethod
def default_store_file():
return True
class StockCsvImport(ModelSQL, ModelView):
"""Import CSV"""
__name__ = 'stock.csv_import'
name = fields.Char('Name', required=True)
model = fields.Many2One('ir.model', 'Model', required=True, domain=[
('model', 'in', ['stock.shipment.in', 'stock.shipment.out.return'])])
date = fields.Function(fields.Date('Date'), 'get_date')
file = fields.Binary('File', required=True, readonly=True,
file_id=file_id, store_prefix=store_prefix)
file_id = fields.Char('File ID', readonly=True)
def get_date(self, name):
return self.create_date

View File

@ -16,5 +16,32 @@
<field name="type">form</field>
<field name="name">shipment_csv_import_form</field>
</record>
<!-- Stock CSV Import -->
<record model="ir.ui.view" id="stock_csv_import_view_list">
<field name="model">stock.csv_import</field>
<field name="type">tree</field>
<field name="name">stock_csv_import_list</field>
</record>
<record model="ir.ui.view" id="stock_csv_import_view_form">
<field name="model">stock.csv_import</field>
<field name="type">form</field>
<field name="name">stock_csv_import_form</field>
</record>
<record model="ir.action.act_window" id="act_stock_csv_import">
<field name="name">CSV Import</field>
<field name="res_model">stock.csv_import</field>
</record>
<record model="ir.action.act_window.view" id="act_stock_csv_import_view1">
<field name="sequence" eval="10"/>
<field name="view" ref="stock_csv_import_view_list"/>
<field name="act_window" ref="act_stock_csv_import"/>
</record>
<record model="ir.action.act_window.view" id="act_stock_csv_import_view2">
<field name="sequence" eval="20"/>
<field name="view" ref="stock_csv_import_view_form"/>
<field name="act_window" ref="act_stock_csv_import"/>
</record>
<menuitem parent="stock.menu_configuration" action="act_stock_csv_import" id="menu_stock_csv_import" icon="tryton-list"/>
</data>
</tryton>

View File

@ -0,0 +1,2 @@
reference,supplier,contact_address,warehouse,effective_date,product,quantity
Reference 1,Supplier,Street 1,Warehouse 1,2016-09-20,Product Error,1
1 reference supplier contact_address warehouse effective_date product quantity
2 Reference 1 Supplier Street 1 Warehouse 1 2016-09-20 Product Error 1

View File

@ -0,0 +1,2 @@
reference,supplier,contact_address,warehouse,effective_date,product,quantity
Reference 1,Supplier,Street 1,Warehouse 1,2016-09-20,Product Ref 2,1
1 reference supplier contact_address warehouse effective_date product quantity
2 Reference 1 Supplier Street 1 Warehouse 1 2016-09-20 Product Ref 2 1

View File

@ -0,0 +1,2 @@
reference,customer,zip_customer,effective_date,warehouse,product,quantity
15,IT00972990709,86020,2019-01-14,WH,Product Ref 1,20
1 reference customer zip_customer effective_date warehouse product quantity
2 15 IT00972990709 86020 2019-01-14 WH Product Ref 1 20

View File

@ -0,0 +1,176 @@
===========================================
Product Cross Reference CSV Import Scenario
===========================================
Imports::
>>> from proteus import Model, Wizard
>>> from trytond.tests.tools import activate_modules
>>> from trytond.modules.stock_csv_import.tests.tools import read_csv_file
>>> import os
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
>>> from decimal import Decimal
>>> import datetime
>>> today = datetime.date.today()
Install stock_csv_import::
>>> config = activate_modules(['stock_csv_import', 'product_cross_reference'])
Create company::
>>> _ = create_company()
>>> company = get_company()
Create Configuration::
>>> Configuration = Model.get('stock.configuration')
>>> config = Configuration()
>>> config.shipment_out_return_csv_headers = "reference,customer.tax_identifier.code,delivery_address.zip,effective_date,warehouse.code,incoming_moves.product.name,incoming_moves.quantity"
>>> config.shipment_in_csv_headers = "reference,supplier.name,contact_address,warehouse.name,effective_date,incoming_moves.product.name,incoming_moves.quantity"
>>> config.save()
Create Parties::
>>> Party = Model.get('party.party')
>>> customer = Party(name='Customer')
>>> customer.save()
>>> customer.addresses[0].zip = '86020'
>>> customer.save()
>>> identifier = customer.identifiers.new()
>>> identifier.code = 'IT00972990709'
>>> identifier.type = 'eu_vat'
>>> customer.save()
>>> supplier = Party(name='Supplier')
>>> address, = supplier.addresses
>>> address.street = 'Street 1'
>>> supplier.save()
Create Products::
>>> ProductUom = Model.get('product.uom')
>>> unit, = ProductUom.find([('name', '=', 'Unit')])
>>> ProductTemplate = Model.get('product.template')
>>> Product = Model.get('product.product')
>>> product1 = Product()
>>> product2 = Product()
>>> template1 = ProductTemplate()
>>> template1.name = 'Product 1'
>>> template1.default_uom = unit
>>> template1.type = 'goods'
>>> template1.purchasable = True
>>> template1.list_price = Decimal('10')
>>> template1.cost_price = Decimal('5')
>>> template1.cost_price_method = 'fixed'
>>> template1.save()
>>> product1.template = template1
>>> product1.save()
>>> template2 = ProductTemplate()
>>> template2.name = 'Product 2'
>>> template2.default_uom = unit
>>> template2.type = 'goods'
>>> template2.purchasable = True
>>> template2.list_price = Decimal('10')
>>> template2.cost_price = Decimal('5')
>>> template2.cost_price_method = 'fixed'
>>> template2.save()
>>> product2.template = template2
>>> product2.save()
Create Product Cross Reference::
>>> CrossReference = Model.get('product.cross_reference')
>>> cross_reference = CrossReference()
>>> cross_reference.party = customer
>>> cross_reference.product = product1
>>> cross_reference.name = 'Product Ref 1'
>>> cross_reference.save()
>>> cross_reference2 = CrossReference()
>>> cross_reference2.party = supplier
>>> cross_reference2.product = product2
>>> cross_reference2.name = 'Product Ref 2'
>>> cross_reference2.save()
CSV import shipment out return wizard::
>>> ShipmentOutReturn = Model.get('stock.shipment.out.return')
>>> Data = Model.get('ir.model.data')
>>> data, = Data.find([
... ('module', '=', 'stock_csv_import'),
... ('fs_id', '=', 'wizard_shipment_out_return_csv_import')])
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})
>>> filename = os.path.join(os.path.dirname(__file__),
... 'product_cross_reference_out_return.csv')
>>> csv_import.form.csv_file = read_csv_file(filename)
>>> csv_import.form.store_file = False
>>> csv_import.execute('import_')
>>> shipment, = ShipmentOutReturn.find([])
>>> move, = shipment.moves
>>> move.product == product1
True
Get stock locations::
>>> Location = Model.get('stock.location')
>>> lost_found_loc, = Location.find([('type', '=', 'lost_found')])
>>> storage_loc, = Location.find([('code', '=', 'STO')])
>>> internal_loc = Location(name='Internal', type='storage')
>>> internal_loc.save()
>>> warehouse_loc, = Location.find([('code', '=', 'WH')])
>>> warehouse_loc.name = 'Warehouse 1'
>>> warehouse_loc.save()
Create Move::
>>> Move = Model.get('stock.move')
>>> move = Move()
>>> move.product = product1
>>> move.quantity = 1
>>> move.from_location = internal_loc
>>> move.to_location = storage_loc
>>> move.currency = company.currency
>>> move.save()
Create Shipment In::
>>> ShipmentIn = Model.get('stock.shipment.in')
>>> shipment_in = ShipmentIn()
>>> shipment_in.effective_date = today
>>> shipment_in.supplier = supplier
>>> shipment_in.warehouse = warehouse_loc
>>> shipment_in.reference = 'Reference 1'
>>> shipment_in.company = company
>>> shipment_in.moves.append(move)
>>> shipment_in.save()
>>> len(shipment_in.moves)
1
CSV import shipment in wizard::
>>> data, = Data.find([
... ('module', '=', 'stock_csv_import'),
... ('fs_id', '=', 'wizard_shipment_in_csv_import')])
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})
>>> filename = os.path.join(os.path.dirname(__file__),
... 'product_cross_reference_in.csv')
>>> csv_import.form.csv_file = read_csv_file(filename)
>>> csv_import.form.store_file = False
>>> csv_import.execute('import_')
>>> shipment_in.reload()
>>> len(shipment_in.moves)
2
>>> shipment_in.moves[0].product == product2
True
When product cross reference does not exist::
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})
>>> filename = os.path.join(os.path.dirname(__file__),
... 'product_cross_reference_error.csv')
>>> csv_import.form.csv_file = read_csv_file(filename)
>>> csv_import.execute('import_')
Traceback (most recent call last):
...
trytond.exceptions.UserError: Cannot find a Product record with value "Product Error". -

View File

@ -108,6 +108,7 @@ Execute Wizard::
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})
>>> filename = os.path.join(os.path.dirname(__file__), 'in.csv')
>>> csv_import.form.csv_file = read_csv_file(filename)
>>> csv_import.form.name = 'CSV Import 1'
>>> csv_import.execute('import_')
>>> shipment_in.reload()
>>> shipment_in.effective_date
@ -117,10 +118,26 @@ Execute Wizard::
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})
>>> filename = os.path.join(os.path.dirname(__file__), 'in.csv')
>>> csv_import.form.csv_file = read_csv_file(filename)
>>> csv_import.form.store_file = False
>>> csv_import.execute('import_')
>>> len(Move.find())
5
Check Stock CSV Import::
>>> IrModel = Model.get('ir.model')
>>> model, = IrModel.find([('model', '=', 'stock.shipment.in')])
>>> CsvImport = Model.get('stock.csv_import')
>>> csv_import, = CsvImport.find([])
>>> csv_import.name
'CSV Import 1'
>>> csv_import.model == model
True
>>> csv_import.file == read_csv_file(filename)
True
>>> csv_import.date == today
True
When product does not exist::
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})

View File

@ -11,6 +11,8 @@ Imports::
>>> from trytond.modules.company.tests.tools import create_company, \
... get_company
>>> from decimal import Decimal
>>> import datetime
>>> today = datetime.date.today()
Install stock_csv_import::
@ -69,6 +71,7 @@ CSV import wizard::
>>> csv_import = Wizard('stock.shipment.csv_import', action={'id': data.db_id})
>>> filename = os.path.join(os.path.dirname(__file__), 'out_return.csv')
>>> csv_import.form.csv_file = read_csv_file(filename)
>>> csv_import.form.name = 'CSV Import 1'
>>> csv_import.execute('import_')
>>> shipment, = ShipmentOutReturn.find([])
>>> shipment.effective_date
@ -86,6 +89,21 @@ CSV import wizard::
>>> move.product.name
'Product 1'
Check Stock CSV Import::
>>> IrModel = Model.get('ir.model')
>>> model, = IrModel.find([('model', '=', 'stock.shipment.out.return')])
>>> CsvImport = Model.get('stock.csv_import')
>>> csv_import, = CsvImport.find([])
>>> csv_import.name
'CSV Import 1'
>>> csv_import.model == model
True
>>> csv_import.file == read_csv_file(filename)
True
>>> csv_import.date == today
True
CSV import wizard when product does not exist::
>>> ShipmentOutReturn = Model.get('stock.shipment.out.return')

View File

@ -29,4 +29,9 @@ def suite():
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
suite.addTests(doctest.DocFileSuite(
'scenario_product_cross_reference_csv_import.rst',
tearDown=doctest_teardown, encoding='utf-8',
checker=doctest_checker,
optionflags=doctest.REPORT_ONLY_FIRST_FAILURE))
return suite

View File

@ -5,6 +5,9 @@ depends:
res
stock
extras_depend:
product_cross_reference
xml:
stock.xml
shipment.xml

View File

@ -8,4 +8,8 @@
<field name="delimiter"/>
<label name="csv_file"/>
<field name="csv_file"/>
<label name="name"/>
<field name="name"/>
<label name="store_file"/>
<field name="store_file"/>
</form>

View File

@ -0,0 +1,11 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<form>
<label name="name"/>
<field name="name"/>
<label name="model"/>
<field name="model"/>
<label name="file"/>
<field name="file"/>
</form>

View File

@ -0,0 +1,9 @@
<?xml version="1.0"?>
<!-- The COPYRIGHT file at the top level of this repository contains the full
copyright notices and license terms. -->
<tree>
<field name="name"/>
<field name="model"/>
<field name="date" widget="date"/>
<field name="date" widget="time" string="Time"/>
</tree>