769 lines
27 KiB
Python
769 lines
27 KiB
Python
# The COPYRIGHT file at the top level of
|
|
# this repository contains the full copyright notices and license terms.
|
|
import csv
|
|
from io import BytesIO
|
|
from sql import Null, Column, Literal
|
|
from sql.conditionals import Coalesce
|
|
from sql.functions import CharLength
|
|
from datetime import datetime, timedelta
|
|
from sql.operators import Concat
|
|
from trytond.model import ModelSQL, ModelView, fields
|
|
from trytond.model import Workflow
|
|
from trytond.pool import PoolMeta, Pool
|
|
from trytond.pyson import Eval, If, Id
|
|
from trytond.tools import grouped_slice
|
|
from trytond.transaction import Transaction
|
|
from trytond.wizard import Wizard, StateTransition, StateView, Button
|
|
from trytond import backend
|
|
|
|
__all__ = ['Configuration', 'ConfigurationSequence', 'Move',
|
|
'ShipmentReconciliation', 'ShipmentReconciliationLine',
|
|
'ShipmentReconcile', 'ShipmentReconcileStart']
|
|
|
|
|
|
class Configuration(metaclass=PoolMeta):
|
|
__name__ = 'stock.configuration'
|
|
|
|
reconcile_sequence = fields.MultiValue(
|
|
fields.Many2One('ir.sequence', 'Shipment reconcile Sequence',
|
|
domain=[
|
|
('company', 'in',
|
|
[Eval('context', {}).get('company', -1), None]),
|
|
('sequence_type', '=', Id('stock_shipment_reconcile',
|
|
'sequence_type_reconcile'))
|
|
], required=True)
|
|
)
|
|
|
|
@classmethod
|
|
def multivalue_model(cls, field):
|
|
pool = Pool()
|
|
if field == 'reconcile_sequence':
|
|
return pool.get('stock.configuration.sequence')
|
|
return super(Configuration, cls).multivalue_model(field)
|
|
|
|
@classmethod
|
|
def default_reconcile_sequence(cls, **pattern):
|
|
return cls.multivalue_model(
|
|
'reconcile_sequence').default_reconcile_sequence()
|
|
|
|
|
|
class ConfigurationSequence(metaclass=PoolMeta):
|
|
__name__ = 'stock.configuration.sequence'
|
|
|
|
reconcile_sequence = fields.Many2One('ir.sequence',
|
|
'Shipment reconcile Sequence',
|
|
domain=[
|
|
('company', 'in',
|
|
[Eval('company', -1), None]),
|
|
('sequence_type', '=', Id('stock_shipment_reconcile',
|
|
'sequence_type_reconcile'))
|
|
],
|
|
depends=['company'],
|
|
required=True)
|
|
|
|
@classmethod
|
|
def __register__(cls, module_name):
|
|
TableHandler = backend.get('TableHandler')
|
|
exist = TableHandler.table_exist(cls._table)
|
|
if exist:
|
|
table = TableHandler(cls, module_name)
|
|
exist &= table.column_exist('reconcile_sequence')
|
|
|
|
super(ConfigurationSequence, cls).__register__(module_name)
|
|
|
|
if not exist:
|
|
# Re-migration
|
|
cls._migrate_property([], [], [])
|
|
|
|
@classmethod
|
|
def default_reconcile_sequence(cls):
|
|
pool = Pool()
|
|
ModelData = pool.get('ir.model.data')
|
|
try:
|
|
return ModelData.get_id('stock_shipment_reconcile',
|
|
'sequence_reconcile')
|
|
except KeyError:
|
|
return None
|
|
|
|
@classmethod
|
|
def _migrate_property(cls, field_names, value_names, fields):
|
|
field_names.append('reconcile_sequence')
|
|
value_names.append('reconcile_sequence')
|
|
super(ConfigurationSequence, cls)._migrate_property(field_names,
|
|
value_names, fields)
|
|
|
|
|
|
class Move(metaclass=PoolMeta):
|
|
__name__ = 'stock.move'
|
|
|
|
reconciled = fields.Boolean('Reconciled', readonly=True)
|
|
reconcile_line = fields.Many2One('stock.shipment.reconciliation.line',
|
|
'Reconcile line', select=True)
|
|
|
|
@staticmethod
|
|
def default_reconciled():
|
|
return False
|
|
|
|
@classmethod
|
|
def copy(cls, records, default=None):
|
|
if default is None:
|
|
default = {}
|
|
default = default.copy()
|
|
default['reconcile_line'] = None
|
|
default['reconciled'] = False
|
|
return super(Move, cls).copy(records, default=default)
|
|
|
|
|
|
class ShipmentReconciliation(Workflow, ModelSQL, ModelView):
|
|
"""Stock Shipment Reconciliation"""
|
|
__name__ = 'stock.shipment.reconciliation'
|
|
_rec_name = 'code'
|
|
|
|
date_ = fields.DateTime('Date', required=True,
|
|
states={'readonly': (Eval('state') != 'draft')},
|
|
depends=['state'])
|
|
code = fields.Char('Code', select=True, readonly=True)
|
|
type_ = fields.Selection([
|
|
('in', 'Supplier Shipment')], 'Shipment type', required=True,
|
|
states={'readonly': (Eval('state') != 'draft') | Eval('lines')},
|
|
depends=['state', 'lines'])
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
states={'readonly': (Eval('state') != 'draft') | Eval('lines')},
|
|
depends=['state', 'lines'])
|
|
lines = fields.One2Many('stock.shipment.reconciliation.line', 'reconcile',
|
|
'Lines', states={'readonly': (Eval('state') != 'draft')},
|
|
depends=['state'])
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('done', 'Done')], 'State', readonly=True)
|
|
discrimination_days = fields.Integer('Discrimination Days', required=True,
|
|
states={'readonly': (Eval('state') != 'draft')},
|
|
depends=['state'])
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ShipmentReconciliation, cls).__setup__()
|
|
cls._transitions |= set((('draft', 'done'),
|
|
('done', 'draft')))
|
|
cls._buttons.update({
|
|
'draft': {
|
|
'icon': 'tryton-back',
|
|
'invisible': (Eval('state') == 'draft'),
|
|
'depends': ['state'],
|
|
},
|
|
'do': {
|
|
'icon': 'tryton-forward',
|
|
'invisible': (Eval('state') != 'draft'),
|
|
'depends': ['state'],
|
|
},
|
|
'import_file': {
|
|
'icon': 'tryton-save',
|
|
'invisible': (Eval('state') != 'draft'),
|
|
'depends': ['state'],
|
|
},
|
|
'clear_lines': {
|
|
'icon': 'tryton-undo',
|
|
'invisible': (Eval('state') != 'draft'),
|
|
'depends': ['state'],
|
|
},
|
|
'validate_lines': {
|
|
'icon': 'tryton-launch',
|
|
'invisible': Eval('state') != 'draft',
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
cls._error_messages.update({
|
|
'cannot_delete': ('Reconciliation "%s" cannot be deleted because '
|
|
'it has lines.'),
|
|
'unreconciled_lines': 'Reconciliation "%s" has unreconcilied lines.'
|
|
})
|
|
|
|
@staticmethod
|
|
def order_code(tables):
|
|
table, _ = tables[None]
|
|
return [CharLength(table.code), table.code]
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@staticmethod
|
|
def default_date_():
|
|
return datetime.now()
|
|
|
|
@staticmethod
|
|
def default_type_():
|
|
return 'in'
|
|
|
|
@staticmethod
|
|
def default_company():
|
|
return Transaction().context.get('company', None)
|
|
|
|
@staticmethod
|
|
def default_discrimination_days():
|
|
return 2
|
|
|
|
@classmethod
|
|
def create(cls, vlist):
|
|
pool = Pool()
|
|
Config = pool.get('stock.configuration')
|
|
|
|
vlist = [x.copy() for x in vlist]
|
|
config = Config(1)
|
|
default_company = cls.default_company()
|
|
for values in vlist:
|
|
if not values.get('code'):
|
|
values['code'] = config.get_multivalue(
|
|
'reconcile_sequence',
|
|
company=values.get('company', default_company)).get()
|
|
return super(ShipmentReconciliation, cls).create(vlist)
|
|
|
|
def _get_shipment_type(self):
|
|
res = {
|
|
'in': ('supplier', 'storage')
|
|
}
|
|
return res[self.type_]
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, records):
|
|
Line = Pool().get('stock.shipment.reconciliation.line')
|
|
for record in records:
|
|
Line.draft([l for l in record.lines if not l.moves])
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('done')
|
|
def do(cls, records):
|
|
cls.validate_lines(records)
|
|
_force_do = False
|
|
|
|
for record in records:
|
|
if not _force_do and any(l.state == 'failed' for l in record.lines):
|
|
cls.raise_user_warning('unreconciled_lines_%s' % record.id,
|
|
'unreconciled_lines', record.rec_name)
|
|
_force_do = True
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def validate_lines(cls, records):
|
|
Line = Pool().get('stock.shipment.reconciliation.line')
|
|
for record in records:
|
|
Line.confirm_try(list(record.lines))
|
|
|
|
@classmethod
|
|
def delete(cls, records):
|
|
for record in records:
|
|
if record.lines:
|
|
cls.raise_user_error('cannot_delete', record.rec_name)
|
|
super(ShipmentReconciliation, cls).delete(records)
|
|
|
|
@classmethod
|
|
@ModelView.button_action('stock_shipment_reconcile.wizard_reconcile')
|
|
def import_file(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def clear_lines(cls, records):
|
|
Line = Pool().get('stock.shipment.reconciliation.line')
|
|
for record in records:
|
|
Line.delete(list(record.lines))
|
|
|
|
def get_limit_dates(self, effective_date):
|
|
return {
|
|
'min_date': effective_date - timedelta(
|
|
days=self.discrimination_days),
|
|
'max_date': effective_date + timedelta(
|
|
days=self.discrimination_days)
|
|
}
|
|
|
|
|
|
class ShipmentReconciliationLine(Workflow, ModelSQL, ModelView):
|
|
"""Stock Shipment Reconciliation line"""
|
|
__name__ = 'stock.shipment.reconciliation.line'
|
|
|
|
effective_date = fields.Date('Effective Date', required=True, readonly=True)
|
|
product = fields.Many2One('product.product', 'Product', required=True,
|
|
readonly=True)
|
|
uom = fields.Many2One('product.uom', 'Uom', required=True,
|
|
readonly=True)
|
|
unit_digits = fields.Function(fields.Integer('Unit Digits'),
|
|
'on_change_with_unit_digits')
|
|
quantity = fields.Float('Quantity', required=True, readonly=True,
|
|
digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits'])
|
|
company = fields.Many2One('company.company', 'Company', required=True,
|
|
readonly=True)
|
|
party = fields.Many2One('party.party', 'Party', required=True, readonly=True)
|
|
from_location = fields.Function(fields.Many2One('stock.location',
|
|
'From Location', depends=['party']), 'on_change_with_from_location')
|
|
shipment_reference = fields.Char('Shipment Reference', readonly=True)
|
|
reconcile = fields.Many2One('stock.shipment.reconciliation',
|
|
'Reconcile', select=True, required=True, readonly=True)
|
|
state = fields.Selection([
|
|
('draft', 'Draft'),
|
|
('confirmed', 'Confirmed'),
|
|
('failed', 'Failed')], 'State', readonly=True)
|
|
shipment_type = fields.Function(
|
|
fields.Char('Shipment type'), 'on_change_with_shipment_type')
|
|
from_location_type = fields.Function(
|
|
fields.Char('From location type'), 'on_change_with_from_location_type')
|
|
to_location_type = fields.Function(
|
|
fields.Char('To location type'), 'on_change_with_to_location_type')
|
|
min_date = fields.Function(
|
|
fields.Date('Mininum date'), 'get_date_limit')
|
|
max_date = fields.Function(
|
|
fields.Date('Maximum date'), 'get_date_limit')
|
|
moves = fields.One2Many('stock.move', 'reconcile_line', 'Moves',
|
|
domain=[If(Eval('state') == 'draft',
|
|
[('reconciled', '=', False),
|
|
('reconcile_line', 'in', (None, Eval('id'))),
|
|
('company', '=', Eval('company')),
|
|
('shipment', 'like', Eval('shipment_type')),
|
|
('effective_date', '>=', Eval('min_date')),
|
|
('effective_date', '<=', Eval('max_date')),
|
|
('state', '=', 'done'),
|
|
('from_location.type', '=', Eval('from_location_type')),
|
|
('to_location.type', '=', Eval('to_location_type')),
|
|
('product', '=', Eval('product')),
|
|
If(Eval('shipment_type') == 'stock.shipment.in,%',
|
|
('shipment.supplier', '=',
|
|
Eval('party'), 'stock.shipment.in'),
|
|
())],
|
|
[])],
|
|
states={'readonly': (Eval('state') != 'draft')},
|
|
depends=['company', 'shipment_type', 'from_location_type', 'id',
|
|
'to_location_type', 'party', 'state', 'min_date', 'max_date',
|
|
'product'])
|
|
move_quantity = fields.Function(
|
|
fields.Float('Moves Qty.', digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_move_quantity')
|
|
diff_quantity = fields.Function(
|
|
fields.Float('Diff. Qty.', digits=(16, Eval('unit_digits', 2)),
|
|
depends=['unit_digits']),
|
|
'get_diff_quantity')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ShipmentReconciliationLine, cls).__setup__()
|
|
cls._transitions |= set((('draft', 'confirmed'),
|
|
('confirmed', 'draft'),
|
|
('draft', 'failed'),
|
|
('failed', 'draft')))
|
|
cls._buttons.update({
|
|
'draft': {
|
|
'icon': 'tryton-back',
|
|
'invisible': (Eval('state') == 'draft'),
|
|
'depends': ['state'],
|
|
},
|
|
'confirm_try': {
|
|
'icon': 'tryton-forward',
|
|
'invisible': (Eval('state') != 'draft'),
|
|
'depends': ['state'],
|
|
},
|
|
})
|
|
cls._error_messages.update({
|
|
'delete_state': 'Lines must be in "Draft" state to be deleted.',
|
|
'delete_reconciled': ('Cannot delete reconciliation line "%s" ' +
|
|
'with stock moves reconciled.')
|
|
})
|
|
|
|
@staticmethod
|
|
def default_state():
|
|
return 'draft'
|
|
|
|
@fields.depends('uom')
|
|
def on_change_with_unit_digits(self, name=None):
|
|
if self.uom:
|
|
return self.uom.digits
|
|
return 2
|
|
|
|
@fields.depends('party')
|
|
def on_change_with_from_location(self):
|
|
if self.party:
|
|
return self.party.supplier_location.id
|
|
|
|
@fields.depends('reconcile')
|
|
def on_change_with_shipment_type(self, name=None):
|
|
if self.reconcile:
|
|
return ('stock.shipment.%s' % self.reconcile.type_) + ',%'
|
|
|
|
@fields.depends('reconcile')
|
|
def on_change_with_from_location_type(self, name=None):
|
|
if self.reconcile:
|
|
return self.reconcile._get_shipment_type()[0]
|
|
|
|
@fields.depends('reconcile')
|
|
def on_change_with_to_location_type(self, name=None):
|
|
if self.reconcile:
|
|
return self.reconcile._get_shipment_type()[1]
|
|
|
|
def get_date_limit(self, name=None):
|
|
res = self.reconcile.get_limit_dates(self.effective_date)
|
|
return res[name]
|
|
|
|
@classmethod
|
|
def get_move_quantity(cls, records, name=None):
|
|
res = {r.id: 0 for r in records}
|
|
for record in records:
|
|
if not record.moves:
|
|
continue
|
|
res[record.id] = record.uom.round(
|
|
sum(m.internal_quantity for m in record.moves))
|
|
return res
|
|
|
|
@classmethod
|
|
def get_diff_quantity(cls, records, name=None):
|
|
return {r.id: r.quantity - r.move_quantity for r in records}
|
|
|
|
@classmethod
|
|
def delete(cls, records):
|
|
for record in records:
|
|
if record.state != 'draft':
|
|
cls.raise_user_error('delete_state')
|
|
if any(m.reconciled for m in record.moves):
|
|
cls.raise_user_error('delete_reconciled', record.rec_name)
|
|
super(ShipmentReconciliationLine, cls).delete(records)
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
@Workflow.transition('draft')
|
|
def draft(cls, records):
|
|
Move = Pool().get('stock.move')
|
|
Move.write([m for r in records for m in r.moves], {'reconciled': False})
|
|
|
|
@classmethod
|
|
@Workflow.transition('confirmed')
|
|
def confirm(cls, records):
|
|
Move = Pool().get('stock.move')
|
|
Move.write([m for r in records for m in r.moves], {'reconciled': True})
|
|
|
|
@classmethod
|
|
@Workflow.transition('failed')
|
|
def fail(cls, records):
|
|
pass
|
|
|
|
@classmethod
|
|
@ModelView.button
|
|
def confirm_try(cls, records):
|
|
to_confirm = []
|
|
to_fail = []
|
|
for record in records:
|
|
if not record.moves:
|
|
to_fail.append(record)
|
|
else:
|
|
to_confirm.append(record)
|
|
if to_confirm:
|
|
cls.confirm(to_confirm)
|
|
if to_fail:
|
|
cls.fail(to_fail)
|
|
|
|
|
|
class ShipmentReconcile(Wizard):
|
|
"""Stock Shipment Reconcile"""
|
|
__name__ = 'stock.shipment.reconcile'
|
|
|
|
start = StateView('stock.shipment.reconcile.start',
|
|
'stock_shipment_reconcile.reconcile_start_view_form',
|
|
[Button('Cancel', 'end', 'tryton-cancel'),
|
|
Button('OK', 'finish_', 'tryton-ok', default=True)])
|
|
finish_ = StateTransition()
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(ShipmentReconcile, cls).__setup__()
|
|
cls._error_messages.update({
|
|
'data_not_found': 'Cannot find data with value "%s".',
|
|
})
|
|
|
|
def _process_file(self):
|
|
pool = Pool()
|
|
Reconcile = pool.get('stock.shipment.reconciliation')
|
|
|
|
csv_file = self.start.file
|
|
|
|
separator = self.start.separator
|
|
if separator == 'tab':
|
|
separator = '\t'
|
|
|
|
csv_file = BytesIO(csv_file)
|
|
csv_file = csv.reader(csv_file, delimiter=str(separator))
|
|
def_values = self._get_file_defaults()
|
|
headers = self._get_file_headers()
|
|
data_fields = self._get_file_m2o_fields()
|
|
|
|
reconcile = Reconcile(Transaction().context['active_id'])
|
|
csv_data = []
|
|
for i, line in enumerate(csv_file):
|
|
if i < 1 or not line:
|
|
continue
|
|
csv_data.append([
|
|
x.decode(self.start.character_encoding).encode('utf-8')
|
|
for x in line])
|
|
data = []
|
|
values = []
|
|
|
|
# read file
|
|
for row in csv_data:
|
|
new_row = {}
|
|
for i, column in enumerate(headers):
|
|
if column in def_values:
|
|
if row[i]:
|
|
def_values[column] = row[i]
|
|
else:
|
|
row[i] = def_values[column]
|
|
new_row[column] = row[i]
|
|
if column in data_fields:
|
|
data_fields[column][1].setdefault(row[i], None)
|
|
data.append(new_row)
|
|
|
|
# find values m2o
|
|
for field_name, field_values in data_fields.items():
|
|
_model = field_values[0]
|
|
if not _model:
|
|
continue
|
|
_Model = pool.get(_model)
|
|
for val in field_values[1].keys():
|
|
if not val:
|
|
continue
|
|
self._get_m2o_value(reconcile, _Model, val, field_name,
|
|
data_fields)
|
|
|
|
used_moves = []
|
|
tables = self.get_move_query_tables()
|
|
for sub_data in grouped_slice(data, count=100):
|
|
where = Literal(False)
|
|
min_date, max_date = None, None
|
|
sub_values = []
|
|
sub_data = list(sub_data)
|
|
for row_data in sub_data:
|
|
res = self._get_temp_line(reconcile, row_data, data_fields)
|
|
sub_values.append(res)
|
|
where |= (self._get_where_clause(reconcile, tables, res))
|
|
if min_date is None or res['effective_date'] < min_date:
|
|
min_date = res['effective_date']
|
|
if max_date is None or res['effective_date'] > max_date:
|
|
max_date = res['effective_date']
|
|
|
|
if sub_values:
|
|
values.extend(sub_values)
|
|
moves = self._get_moves(tables, where)
|
|
|
|
for move in moves:
|
|
for row_data in sub_values:
|
|
if (self._move_matches(reconcile, row_data, move) and
|
|
move.id not in used_moves):
|
|
row_data['move'] = move.id
|
|
used_moves.append(move.id)
|
|
break
|
|
|
|
return values
|
|
|
|
@classmethod
|
|
def _get_file_defaults(cls):
|
|
return {
|
|
'effective_date': None,
|
|
'shipment': None,
|
|
'party': None,
|
|
'product': None
|
|
}
|
|
|
|
@classmethod
|
|
def _get_file_headers(cls):
|
|
return ['effective_date', 'shipment', 'party', 'product', 'quantity']
|
|
|
|
@classmethod
|
|
def _get_file_m2o_fields(cls):
|
|
return {
|
|
'party': ['party.party', {}],
|
|
'product': ['product.product', {}]
|
|
}
|
|
|
|
def _get_m2o_value(self, reconcile, Model, value, field_name, data_fields):
|
|
res = Model.search([('name', '=', value)], limit=1)
|
|
if not res:
|
|
self.raise_user_error('data_not_found', value.decode(
|
|
self.start.character_encoding))
|
|
res, = res
|
|
data_fields[field_name][1][value] = res
|
|
|
|
def _get_temp_line(self, reconcile, row, data_fields):
|
|
return {
|
|
'effective_date': datetime.strptime(
|
|
row['effective_date'], '%Y-%m-%d').date(),
|
|
'company': reconcile.company.id,
|
|
'party': data_fields['party'][1][row['party']].id,
|
|
'shipment_reference': row['shipment'],
|
|
'product': data_fields['product'][1][row['product']].id,
|
|
'quantity': float(row['quantity']),
|
|
'uom': data_fields['product'][1][row['product']].default_uom.id,
|
|
'move': None,
|
|
}
|
|
|
|
def _get_where_clause(self, reconcile, tables, data):
|
|
_dates = reconcile.get_limit_dates(data['effective_date'])
|
|
party_field = {
|
|
'in': 'supplier',
|
|
}
|
|
return (
|
|
(tables['move'].product == data['product']) &
|
|
(Column(tables['shipment'], party_field[reconcile.type_]) == data['party']) &
|
|
(tables['shipment'].effective_date >= _dates['min_date']) &
|
|
(tables['shipment'].effective_date <= _dates['max_date']) &
|
|
(tables['shipment'].reference == data['shipment_reference'])
|
|
)
|
|
|
|
def get_move_query_tables(self):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Location = pool.get('stock.location')
|
|
Reconcile = pool.get('stock.shipment.reconciliation')
|
|
move = Move.__table__()
|
|
loc1 = Location.__table__()
|
|
loc2 = Location.__table__()
|
|
|
|
reconcile = Reconcile(Transaction().context['active_id'])
|
|
Shipment = pool.get('stock.shipment.%s' % reconcile.type_)
|
|
shipment = Shipment.__table__()
|
|
return {
|
|
'move': move,
|
|
'from_location': loc1,
|
|
'to_location': loc2,
|
|
'shipment': shipment
|
|
}
|
|
|
|
def _get_moves(self, tables, where):
|
|
pool = Pool()
|
|
Move = pool.get('stock.move')
|
|
Reconcile = pool.get('stock.shipment.reconciliation')
|
|
|
|
reconcile = Reconcile(Transaction().context['active_id'])
|
|
Shipment = pool.get('stock.shipment.%s' % reconcile.type_)
|
|
company_id = Transaction().context.get('company')
|
|
locs = reconcile._get_shipment_type()
|
|
cursor = Transaction().cursor
|
|
|
|
query = tables['move'].join(
|
|
tables['from_location'], condition=(
|
|
tables['from_location'].id == tables['move'].from_location)
|
|
).join(tables['to_location'], condition=(
|
|
tables['to_location'].id == tables['move'].to_location)
|
|
).join(tables['shipment'], condition=(
|
|
Concat('%s,' % Shipment.__name__, tables['shipment'].id) == tables['move'].shipment)
|
|
).select(tables['move'].id,
|
|
where=(
|
|
(tables['move'].company == company_id) &
|
|
(tables['move'].state == 'done') &
|
|
(Coalesce(tables['move'].reconciled, False) == False) &
|
|
(tables['move'].reconcile_line == Null) &
|
|
(tables['from_location'].type == locs[0]) &
|
|
(tables['to_location'].type == locs[1]) &
|
|
(where))
|
|
)
|
|
cursor.execute(*query)
|
|
move_ids = cursor.fetchall()
|
|
if move_ids:
|
|
return Move.browse([m[0] for m in move_ids])
|
|
return []
|
|
|
|
def _move_matches(self, reconcile, data, move):
|
|
data_res, move_res = self._get_move_match(data, move)
|
|
if data_res == move_res:
|
|
if move.effective_date == data['effective_date']:
|
|
return True
|
|
_dates = reconcile.get_limit_dates(data['effective_date'])
|
|
if (move.effective_date <= _dates['max_date'] and
|
|
move.effective_date >= _dates['min_date']):
|
|
return True
|
|
return False
|
|
|
|
def _get_move_match(self, data, move):
|
|
data_res = {
|
|
'product': data['product'],
|
|
'party': data['party'],
|
|
'shipment_reference': data['shipment_reference'],
|
|
}
|
|
move_res = {
|
|
'product': move.product.id,
|
|
'party': move.party.id,
|
|
'shipment_reference': move.shipment.reference,
|
|
}
|
|
return data_res, move_res
|
|
|
|
def transition_finish_(self):
|
|
pool = Pool()
|
|
ReconcileLine = pool.get('stock.shipment.reconciliation.line')
|
|
Reconcile = pool.get('stock.shipment.reconciliation')
|
|
Attachment = pool.get('ir.attachment')
|
|
|
|
reconciliation = Reconcile(Transaction().context['active_id'])
|
|
to_create = []
|
|
moves = self._process_file()
|
|
|
|
for move in moves:
|
|
line = self._get_reconcile_line(
|
|
ReconcileLine, reconciliation, move)
|
|
to_create.append(line)
|
|
|
|
if to_create:
|
|
with Transaction().set_context(_check_access=False):
|
|
ReconcileLine.save(to_create)
|
|
|
|
if self.start.attach_file:
|
|
attach = Attachment(
|
|
name=datetime.now().strftime("%y/%m/%d %H:%M:%S"),
|
|
type='data',
|
|
data=self.start.file,
|
|
resource=str(reconciliation))
|
|
attach.save()
|
|
return 'end'
|
|
|
|
def _get_reconcile_line(self, Reconcile, reconciliation, move):
|
|
res = Reconcile(
|
|
reconcile=reconciliation,
|
|
effective_date=move['effective_date'],
|
|
company=move['company'],
|
|
party=move['party'],
|
|
shipment_reference=move['shipment_reference'],
|
|
product=move['product'],
|
|
quantity=move['quantity'],
|
|
uom=move['uom'],
|
|
state='draft')
|
|
if move['move']:
|
|
res.moves = [move['move']]
|
|
return res
|
|
|
|
|
|
class ShipmentReconcileStart(ModelView):
|
|
"""Stock Shipment Reconcile Start"""
|
|
__name__ = 'stock.shipment.reconcile.start'
|
|
|
|
file = fields.Binary('File', required=True)
|
|
attach_file = fields.Boolean('Attach file')
|
|
separator = fields.Selection([
|
|
(',', 'Comma'),
|
|
(';', 'Semicolon'),
|
|
('tab', 'Tabulator'),
|
|
('|', '|'),
|
|
], 'CSV Separator', required=True,
|
|
help='Field separator in CSV lines.')
|
|
character_encoding = fields.Selection([
|
|
('utf-8', 'UTF-8'),
|
|
('latin-1', 'Latin-1'),
|
|
], 'CSV Character Encoding', required=True)
|
|
|
|
@staticmethod
|
|
def default_attach_file():
|
|
return True
|
|
|
|
@staticmethod
|
|
def default_separator():
|
|
return ','
|
|
|
|
@staticmethod
|
|
def default_character_encoding():
|
|
return 'utf-8'
|