Merge lp:~unifield-team/unifield-server/US-3501-physical-inventory into lp:unifield-server

Proposed by jftempo
Status: Merged
Merged at revision: 4623
Proposed branch: lp:~unifield-team/unifield-server/US-3501-physical-inventory
Merge into: lp:unifield-server
Diff against target: 4532 lines (+4287/-15)
23 files modified
bin/addons/stock/__init__.py (+1/-0)
bin/addons/stock/__openerp__.py (+7/-1)
bin/addons/stock/physical_inventory.py (+1345/-0)
bin/addons/stock/physical_inventory_data.xml (+17/-0)
bin/addons/stock/physical_inventory_view.xml (+267/-0)
bin/addons/stock/report/__init__.py (+2/-1)
bin/addons/stock/report/physical_inventory_counting_sheet.py (+61/-0)
bin/addons/stock/report/physical_inventory_counting_sheet.rml (+199/-0)
bin/addons/stock/report/physical_inventory_counting_sheet.xml (+530/-0)
bin/addons/stock/report/physical_inventory_discrepancies_report.py (+42/-0)
bin/addons/stock/report/physical_inventory_discrepancies_report.xml (+882/-0)
bin/addons/stock/report/physical_inventory_view.xml (+30/-0)
bin/addons/stock/stock_view.xml (+0/-1)
bin/addons/stock/wizard/__init__.py (+3/-0)
bin/addons/stock/wizard/physical_inventory_generate_counting_sheet.py (+198/-0)
bin/addons/stock/wizard/physical_inventory_generate_counting_sheet_view.xml (+17/-0)
bin/addons/stock/wizard/physical_inventory_import.py (+105/-0)
bin/addons/stock/wizard/physical_inventory_import_view.xml (+38/-0)
bin/addons/stock/wizard/physical_inventory_select_products.py (+413/-0)
bin/addons/stock/wizard/physical_inventory_select_products_view.xml (+74/-0)
bin/addons/stock_override/wizard/stock_card_wizard.py (+47/-5)
bin/addons/useability_dashboard_and_menu/menu/warehouse_menu.xml (+6/-7)
bin/osv/orm.py (+3/-0)
To merge this branch: bzr merge lp:~unifield-team/unifield-server/US-3501-physical-inventory
Reviewer Review Type Date Requested Status
UniField Reviewer Team Pending
Review via email: mp+334052@code.launchpad.net
To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'bin/addons/stock/__init__.py'
2--- bin/addons/stock/__init__.py 2011-01-14 00:11:01 +0000
3+++ bin/addons/stock/__init__.py 2017-11-24 09:38:22 +0000
4@@ -24,5 +24,6 @@
5 import product
6 import report
7 import wizard
8+import physical_inventory
9
10 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
11\ No newline at end of file
12
13=== modified file 'bin/addons/stock/__openerp__.py'
14--- bin/addons/stock/__openerp__.py 2017-09-28 14:05:02 +0000
15+++ bin/addons/stock/__openerp__.py 2017-11-24 09:38:22 +0000
16@@ -70,7 +70,13 @@
17 "partner_view.xml",
18 "report/report_stock_move_view.xml",
19 "report/report_stock_view.xml",
20- "board_warehouse_view.xml"
21+ "board_warehouse_view.xml",
22+ "physical_inventory_view.xml",
23+ "physical_inventory_data.xml",
24+ "report/physical_inventory_view.xml",
25+ "wizard/physical_inventory_select_products_view.xml",
26+ "wizard/physical_inventory_generate_counting_sheet_view.xml",
27+ "wizard/physical_inventory_import_view.xml",
28 ],
29 'installable': True,
30 'active': False,
31
32=== added file 'bin/addons/stock/physical_inventory.py'
33--- bin/addons/stock/physical_inventory.py 1970-01-01 00:00:00 +0000
34+++ bin/addons/stock/physical_inventory.py 2017-11-24 09:38:22 +0000
35@@ -0,0 +1,1345 @@
36+# -*- coding: utf-8 -*-
37+
38+import base64
39+import time
40+from dateutil.parser import parse
41+import math
42+
43+import decimal_precision as dp
44+from spreadsheet_xml.spreadsheet_xml import SpreadsheetXML
45+
46+from osv import fields, osv
47+from tools.misc import DEFAULT_SERVER_DATETIME_FORMAT, DEFAULT_SERVER_DATE_FORMAT
48+from tools.translate import _
49+
50+
51+PHYSICAL_INVENTORIES_STATES = (
52+ ('draft', _('Draft')),
53+ ('counting', _('Counting')),
54+ ('counted', _('Counted')),
55+ ('validated', _('Validated')),
56+ ('confirmed', _('Confirmed')),
57+ ('closed', _('Closed')),
58+ ('cancel', _('Cancelled'))
59+)
60+
61+
62+class NegativeValueError(ValueError):
63+ """Negative value Exception"""
64+
65+
66+class PhysicalInventory(osv.osv):
67+ _name = 'physical.inventory'
68+ _description = 'Physical Inventory'
69+
70+ def _inventory_totals(self, cr, uid, ids, field_names, arg, context=None):
71+ context = context is None and {} or context
72+ def read_many(model, ids, columns):
73+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
74+ def search(model, domain):
75+ return self.pool.get(model).search(cr, uid, domain, context=context)
76+
77+ inventories = read_many("physical.inventory", ids, ["discrepancy_line_ids",
78+ "counting_line_ids"])
79+
80+ totals = {}
81+ for inventory in inventories:
82+
83+ counting_lines = read_many("physical.inventory.counting",
84+ inventory["counting_line_ids"],
85+ ["quantity", "standard_price"])
86+
87+ # Keep only non-ignored lines
88+ discrepancy_line_ids = search("physical.inventory.discrepancy",
89+ ['&',
90+ ('ignored', '!=', True),
91+ ("id", "in", inventory["discrepancy_line_ids"])])
92+
93+ discrepancy_lines = read_many("physical.inventory.discrepancy",
94+ discrepancy_line_ids,
95+ ["discrepancy_value"])
96+
97+ inventory_lines_value = 0
98+ inventory_lines_absvalue = 0
99+ for l in counting_lines:
100+ try:
101+ inventory_lines_value += float(l["quantity"]) * float(l["standard_price"])
102+ inventory_lines_absvalue += abs(float(l["quantity"])) * float(l["standard_price"])
103+ except:
104+ # Most likely we couldnt parse the quantity / price...
105+ pass
106+
107+ discrepancy_lines_value = 0
108+ discrepancy_lines_absvalue = 0
109+ for l in discrepancy_lines:
110+ try:
111+ discrepancy_lines_value += float(l["discrepancy_value"])
112+ discrepancy_lines_absvalue += abs(float(l["discrepancy_value"]))
113+ except:
114+ # Most likely we couldnt parse the quantity / price...
115+ pass
116+
117+ total = {
118+ 'inventory_lines_number': len(counting_lines),
119+ 'discrepancy_lines_number': len(discrepancy_lines),
120+ 'inventory_lines_value': inventory_lines_value,
121+ 'discrepancy_lines_value': discrepancy_lines_value,
122+ 'inventory_lines_absvalue': inventory_lines_absvalue,
123+ 'discrepancy_lines_absvalue':discrepancy_lines_absvalue
124+ }
125+
126+ total['discrepancy_lines_percent'] = 100 * total['discrepancy_lines_number'] / total['inventory_lines_number'] if total['inventory_lines_number'] else 0.0
127+ total['discrepancy_lines_percent_value'] = 100 * total['discrepancy_lines_value'] / total['inventory_lines_value'] if total['inventory_lines_value'] else 0.0
128+ total['discrepancy_lines_percent_absvalue'] = 100 * total['discrepancy_lines_absvalue'] / total['inventory_lines_absvalue'] if total['inventory_lines_absvalue'] else 0.0
129+
130+ totals[inventory["id"]] = total
131+
132+ return totals
133+
134+
135+ _columns = {
136+ 'ref': fields.char('Reference', size=64, readonly=True),
137+ 'name': fields.char('Name', size=64, required=True, readonly=True, states={'draft': [('readonly', False)]}),
138+ 'date': fields.datetime('Creation Date', required=True, readonly=True, states={'draft': [('readonly', False)]}),
139+ 'responsible': fields.char('Responsible', size=128, required=False),
140+ 'date_done': fields.datetime('Date done', readonly=True),
141+ 'product_ids': fields.many2many('product.product', 'physical_inventory_product_rel',
142+ 'product_id', 'inventory_id', string="Product selection"),
143+ 'discrepancy_line_ids': fields.one2many('physical.inventory.discrepancy', 'inventory_id', 'Discrepancy lines',
144+ states={'closed': [('readonly', True)]}),
145+ 'counting_line_ids': fields.one2many('physical.inventory.counting', 'inventory_id', 'Counting lines',
146+ states={'closed': [('readonly', True)]}),
147+ 'location_id': fields.many2one('stock.location', 'Location', required=True, readonly=True,
148+ states={'draft': [('readonly', False)]}),
149+ 'move_ids': fields.many2many('stock.move', 'physical_inventory_move_rel', 'inventory_id', 'move_id',
150+ 'Created Moves', readonly=True),
151+ 'state': fields.selection(PHYSICAL_INVENTORIES_STATES, 'State', readonly=True, select=True),
152+ 'company_id': fields.many2one('res.company', 'Company', readonly=True, select=True, required=True,
153+ states={'draft': [('readonly', False)]}),
154+ 'full_inventory': fields.boolean('Full inventory', readonly=True),
155+ 'file_to_import': fields.binary(string='File to import', filters='*.xml'),
156+ 'file_to_import2': fields.binary(string='File to import', filters='*.xml'),
157+
158+ # Total for product
159+ 'inventory_lines_number': fields.function(_inventory_totals, multi="inventory_total", method=True, type='integer', string=_("Number of inventory lines")),
160+ 'discrepancy_lines_number': fields.function(_inventory_totals, multi="inventory_total", method=True, type='integer', string=_("Number of discrepancy lines")),
161+ 'discrepancy_lines_percent': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Percent of lines with discrepancies")),
162+ 'inventory_lines_value': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Total value of inventory")),
163+ 'discrepancy_lines_value': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Value of discrepancies")),
164+ 'discrepancy_lines_percent_value': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Percent of value of discrepancies")),
165+ 'inventory_lines_absvalue': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Absolute value of inventory")),
166+ 'discrepancy_lines_absvalue': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Absolute value of discrepancies")),
167+ 'discrepancy_lines_percent_absvalue': fields.function(_inventory_totals, multi="inventory_total", method=True, type='float', string=_("Percent of absolute value of discrepancies")),
168+ }
169+
170+ _defaults = {
171+ 'ref': False,
172+ 'date': lambda *a: time.strftime(DEFAULT_SERVER_DATETIME_FORMAT),
173+ 'state': 'draft',
174+ 'full_inventory': False,
175+ 'company_id': lambda self, cr, uid,
176+ c: self.pool.get('res.company')._company_default_get(cr, uid, 'physical.inventory', context=c)
177+ }
178+
179+ _order = "ref desc, date desc"
180+
181+ def create(self, cr, uid, values, context):
182+ context = context is None and {} or context
183+ values["ref"] = self.pool.get('ir.sequence').get(cr, uid, 'physical.inventory')
184+
185+ return super(PhysicalInventory, self).create(cr, uid, values, context=context)
186+
187+
188+ def copy(self, cr, uid, id_, default=None, context=None):
189+ default = default is None and {} or default
190+ context = context is None and {} or context
191+ default = default.copy()
192+
193+ default['state'] = 'draft'
194+ default['date'] = time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)
195+ fields_to_empty = ["ref",
196+ "full_inventory",
197+ "date_done",
198+ "file_to_import",
199+ "file_to_import2",
200+ "counting_line_ids",
201+ "discrepancy_line_ids",
202+ "move_ids"]
203+
204+ for field in fields_to_empty:
205+ default[field] = False
206+
207+ return super(PhysicalInventory, self).copy(cr, uid, id_, default, context=context)
208+
209+
210+ def perm_write(self, cr, user, ids, fields, context=None):
211+ pass
212+
213+
214+ def set_full_inventory(self, cr, uid, ids, context=None):
215+ context = context is None and {} or context
216+
217+ # Set full inventory as true and unlink all products already selected
218+ self.write(cr, uid, ids, {'full_inventory': True,
219+ 'product_ids': [(5)]}, context=context)
220+ return {}
221+
222+
223+ def action_select_products(self, cr, uid, ids, context=None):
224+ """
225+ Trigerred when clicking on the button "Products Select"
226+
227+ Open the wizard to select the products according to specific filters..
228+ """
229+ context = context is None and {} or context
230+ def read_single(model, id_, column):
231+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
232+ def create(model, vals):
233+ return self.pool.get(model).create(cr, uid, vals, context=context)
234+ def view(module, view):
235+ return self.pool.get('ir.model.data').get_object_reference(cr, uid, module, view)[1]
236+
237+ # Prepare values to feed the wizard with
238+ assert len(ids) == 1
239+ inventory_id = ids[0]
240+
241+ # Is it a full inventory ?
242+ full_inventory = read_single(self._name, inventory_id, 'full_inventory')
243+
244+ # Create the wizard
245+ wiz_model = 'physical.inventory.select.products'
246+ wiz_values = {"inventory_id": inventory_id,
247+ "full_inventory": full_inventory }
248+ wiz_id = create(wiz_model, wiz_values)
249+ context['wizard_id'] = wiz_id
250+
251+ # Get the view reference
252+ view_id = view('stock', 'physical_inventory_select_products')
253+
254+ # Return a description of the wizard view
255+ return {'type': 'ir.actions.act_window',
256+ 'target': 'new',
257+ 'res_model': wiz_model,
258+ 'res_id': wiz_id,
259+ 'view_id': [view_id],
260+ 'view_type': 'form',
261+ 'view_mode': 'form',
262+ 'context': context}
263+
264+ def generate_counting_sheet(self, cr, uid, ids, context=None):
265+ """
266+ Trigerred when clicking on the button "Generate counting sheet"
267+
268+ Open the wizard to fill the counting sheet with selected products.
269+ Choose to include batch numbers / expiry date or not
270+ """
271+ context = context is None and {} or context
272+ def read_single(model, id_, column):
273+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
274+ def create(model, vals):
275+ return self.pool.get(model).create(cr, uid, vals, context=context)
276+ def view(module, view):
277+ return self.pool.get('ir.model.data').get_object_reference(cr, uid, module, view)[1]
278+
279+ # Prepare values to feed the wizard with
280+ assert len(ids) == 1
281+ inventory_id = ids[0]
282+
283+ # Create the wizard
284+ wiz_model = 'physical.inventory.generate.counting.sheet'
285+ wiz_values = {"inventory_id": inventory_id}
286+ wiz_id = create(wiz_model, wiz_values)
287+ context['wizard_id'] = wiz_id
288+
289+ # Get the view reference
290+ view_id = view('stock', 'physical_inventory_generate_counting_sheet')
291+
292+ # Return a description of the wizard view
293+ return {'type': 'ir.actions.act_window',
294+ 'target': 'new',
295+ 'res_model': wiz_model,
296+ 'res_id': wiz_id,
297+ 'view_id': [view_id],
298+ 'view_type': 'form',
299+ 'view_mode': 'form',
300+ 'context': context}
301+
302+
303+ def generate_discrepancies(self, cr, uid, inventory_ids, context=None):
304+ """
305+ Trigerred when clicking on the button "Finish counting"
306+
307+ Analyze the counted lines to look for discrepancy, and fill the
308+ 'discrepancy lines' accordingly.
309+ """
310+ context = context if context else {}
311+ def read_single(model, id_, column):
312+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
313+ def read_many(model, ids, columns):
314+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
315+ def write(model, id_, vals):
316+ return self.pool.get(model).write(cr, uid, [id_], vals, context=context)
317+ def write_many(model, ids, vals):
318+ return self.pool.get(model).write(cr, uid, ids, vals, context=context)
319+
320+ # Get this inventory...
321+ assert len(inventory_ids) == 1
322+ inventory_id = inventory_ids[0]
323+
324+ # Get the location and counting lines
325+ inventory = read_many(self._name, [inventory_id], [ "location_id",
326+ "discrepancy_line_ids",
327+ "counting_line_ids" ])[0]
328+
329+ location_id = inventory["location_id"][0]
330+ counting_line_ids = inventory["counting_line_ids"]
331+
332+ counting_lines = read_many('physical.inventory.counting',
333+ counting_line_ids,
334+ [ "line_no",
335+ "product_id",
336+ "product_uom_id",
337+ "standard_price",
338+ "currency_id",
339+ "batch_number",
340+ "expiry_date",
341+ "quantity"])
342+
343+ # Extract the list of (unique) product ids
344+ product_ids = [ line["product_id"][0] for line in counting_lines ]
345+ product_ids = list(set(product_ids))
346+
347+ # Fetch the theoretical quantities
348+ # This will be a dict like { (product_id_1, BN_string_1) : theo_qty_1,
349+ # (product_id_2, BN_string_2) : theo_qty_2 }
350+ theoretical_quantities = self.get_stock_for_products_at_location(cr, uid, product_ids, location_id, context=context)
351+
352+ # Create a similar dictionnary for counted quantities
353+ counting_lines_per_product_batch_expirtydate = {}
354+ counted_quantities = {}
355+ for line in counting_lines:
356+
357+ product_batch_expirydate = (line["product_id"][0],
358+ line["batch_number"] or False,
359+ line["expiry_date"])
360+
361+ qty = float(line["quantity"]) if line["quantity"] else False
362+ counted_quantities[product_batch_expirydate] = qty
363+
364+ counting_lines_per_product_batch_expirtydate[product_batch_expirydate] = {
365+ "line_id": line["id"],
366+ "line_no": line["line_no"]
367+ }
368+
369+ # Create a similar dictionnary for existing discrepancies
370+ previous_discrepancy_line_ids = inventory["discrepancy_line_ids"]
371+ previous_discrepancy_lines = read_many('physical.inventory.discrepancy',
372+ previous_discrepancy_line_ids,
373+ [ "product_id",
374+ "batch_number",
375+ "expiry_date",
376+ "ignored" ])
377+
378+ previous_discrepancies = {}
379+ for line in previous_discrepancy_lines:
380+ product_batch_expirydate = (line["product_id"][0],
381+ line["batch_number"] or False,
382+ line["expiry_date"])
383+ previous_discrepancies[product_batch_expirydate] = {
384+ "id": line["id"],
385+ "ignored": line["ignored"],
386+ "todelete": True
387+ }
388+
389+ ###################################################
390+ # Now, compare theoretical and counted quantities #
391+ ###################################################
392+
393+ # First, create a unique set containing all product/batches
394+ all_product_batch_expirydate = set().union(theoretical_quantities,
395+ counted_quantities)
396+
397+ new_discrepancies = []
398+ update_discrepancies = {}
399+ counting_lines_with_no_discrepancy = []
400+
401+ # For each of them, compare the theoretical and counted qty
402+ for product_batch_expirydate in all_product_batch_expirydate:
403+
404+ # If the key is not known, assume 0
405+ theoretical_qty = theoretical_quantities.get(product_batch_expirydate, 0.0)
406+ counted_qty = counted_quantities.get(product_batch_expirydate, -1.0)
407+
408+ # If no discrepancy, nothing to do
409+ # (Use a continue to save 1 indentation level..)
410+ if counted_qty == theoretical_qty:
411+ if product_batch_expirydate in counting_lines_per_product_batch_expirtydate:
412+ counting_line_id = counting_lines_per_product_batch_expirtydate[product_batch_expirydate]["line_id"]
413+ counting_lines_with_no_discrepancy.append(counting_line_id)
414+ continue
415+
416+ # If this product/batch is known in the counting line, use
417+ # the existing line number
418+ if product_batch_expirydate in counting_lines_per_product_batch_expirtydate:
419+ this_product_batch_expirydate = counting_lines_per_product_batch_expirtydate[product_batch_expirydate]
420+ line_no = this_product_batch_expirydate["line_no"]
421+
422+ # Otherwise, create additional line numbers starting from
423+ # the total of existing lines
424+ else:
425+ # FIXME : propably does not guarrantee uniqueness and
426+ # 'incremenctaliness' of line_no in some edge cases when
427+ # discrepancy report is regenerated multiple times...
428+ line_no = len(counted_quantities) + 1 + len(new_discrepancies)
429+
430+ if product_batch_expirydate in previous_discrepancies:
431+ previous_discrepancies[product_batch_expirydate]["todelete"] = False
432+ existing_id = previous_discrepancies[product_batch_expirydate]["id"]
433+ update_discrepancies[existing_id] = {
434+ "line_no": line_no,
435+ "counted_qty": counted_qty
436+ }
437+ else:
438+ new_discrepancies.append( \
439+ { "inventory_id": inventory_id,
440+ "line_no": line_no,
441+ "product_id": product_batch_expirydate[0],
442+ "batch_number": product_batch_expirydate[1],
443+ "expiry_date": product_batch_expirydate[2],
444+ "theoretical_qty": theoretical_qty,
445+ "counted_qty": counted_qty
446+ })
447+
448+ # Update discrepancy flags on counting lines
449+ counting_lines_with_discrepancy = [ l["id"] for l in counting_lines if not l["id"] in counting_lines_with_no_discrepancy ]
450+ write_many("physical.inventory.counting", counting_lines_with_discrepancy, {"discrepancy": True})
451+ write_many("physical.inventory.counting", counting_lines_with_no_discrepancy, {"discrepancy": False})
452+
453+ # Sort discrepancies according to line number
454+ new_discrepancies = sorted(new_discrepancies, key=lambda d: d["line_no"])
455+
456+ # Prepare the actual create/remove for discrepancy lines
457+ # 0 is for addition/creation
458+ # 1 is for update
459+ # 2 is the code for removal/deletion
460+
461+ create_discrepancy_lines = [ (0,0,discrepancy) for discrepancy in new_discrepancies ]
462+ update_discrepancy_lines = [ (1,id_,values) for id_, values in update_discrepancies.items() ]
463+ delete_discrepancy_lines = [ (2,line["id"]) for line in previous_discrepancies.values() if line["todelete"] ]
464+
465+ todo = []
466+ todo.extend(delete_discrepancy_lines)
467+ todo.extend(update_discrepancy_lines)
468+ todo.extend(create_discrepancy_lines)
469+
470+ # Do the actual write
471+ write("physical.inventory", inventory_id, {'discrepancy_line_ids': todo})
472+
473+
474+ self._update_total_product(cr, uid, inventory_id,
475+ theoretical_quantities,
476+ counted_quantities,
477+ context=context)
478+
479+ return self.resolve_discrepancies_anomalies(cr, uid, inventory_id, context=context)
480+
481+
482+ def resolve_discrepancies_anomalies(self, cr, uid, inventory_id, context=None):
483+ context = context if context else {}
484+ def read_single(model, id_, column):
485+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
486+ def read_many(model, ids, columns):
487+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
488+ def product_identity_str(line):
489+ str_ = "product '%s'" % line["product_id"][1]
490+ if line["batch_number"] or line["expiry_date"]:
491+ str_ += " with Batch number '%s' and Expiry date '%s'" % (line["batch_number"] or '', line["expiry_date"] or '')
492+ else:
493+ str_ += " (no batch number / expiry date)"
494+ return str_
495+
496+ discrepancy_line_ids = read_single("physical.inventory", inventory_id, 'discrepancy_line_ids')
497+
498+ discrepancy_lines = read_many('physical.inventory.discrepancy',
499+ discrepancy_line_ids,
500+ [ "line_no",
501+ "product_id",
502+ "batch_number",
503+ "expiry_date",
504+ "counted_qty",
505+ "ignored"])
506+
507+ anomalies = []
508+ for line in discrepancy_lines:
509+ if line["ignored"]:
510+ continue
511+ anomaly = False
512+ if line["counted_qty"] == False:
513+ anomaly = "Quantity for line %s, %s is incorrect." % (line["line_no"], product_identity_str(line))
514+ if line["counted_qty"] < 0.0:
515+ anomaly = "A line for %s was expected but not found." % product_identity_str(line)
516+
517+ if anomaly:
518+ anomalies.append({"message": anomaly + " Ignore line or count as 0 ?",
519+ "line_id": line["id"]})
520+
521+ if anomalies:
522+ return self.pool.get('physical.inventory.import.wizard').action_box(cr, uid, 'Warning', anomalies)
523+ else:
524+ return {}
525+
526+
527+ def _update_total_product(self, cr, uid, inventory_id, theoretical_qties, counted_qties, context=None):
528+ """
529+ theoretical_qties and counted_qties are indexed with (product_id, batchnumber, expirydate)
530+ """
531+ def read_single(model, id_, column):
532+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
533+ def read_many(model, ids, columns):
534+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
535+ def write(model, id_, vals):
536+ return self.pool.get(model).write(cr, uid, [id_], vals, context=context)
537+
538+ discrepancy_line_ids = read_single("physical.inventory",
539+ inventory_id,
540+ 'discrepancy_line_ids')
541+
542+ discrepancy_lines = read_many("physical.inventory.discrepancy",
543+ discrepancy_line_ids,
544+ ["product_id"])
545+
546+ all_product_ids = set([ l["product_id"][0] for l in discrepancy_lines ])
547+
548+ total_product_theoretical_qties = {}
549+ total_product_counted_qties = {}
550+ for product_id in all_product_ids:
551+
552+ # FIXME : how to not take into account ignored lines in the count ? :/
553+ total_product_theoretical_qties[product_id] = sum([ qty for k, qty in theoretical_qties.items() if k[0] == product_id ])
554+ total_product_counted_qties[product_id] = sum([ qty for k, qty in counted_qties.items() if k[0] == product_id ])
555+
556+ update_discrepancy_lines = {}
557+ for line in discrepancy_lines:
558+ id_ = line["id"]
559+ product_id = line["product_id"][0]
560+ update_discrepancy_lines[id_] = {
561+ 'total_product_theoretical_qty': total_product_theoretical_qties[product_id],
562+ 'total_product_counted_qty': total_product_counted_qties[product_id]
563+ }
564+
565+ todo = [ (1,id_,values) for id_, values in update_discrepancy_lines.items() ]
566+
567+ write("physical.inventory", inventory_id, {'discrepancy_line_ids':todo})
568+
569+
570+ def pre_process_discrepancies(self, cr, uid, items, context=None):
571+ discrepancies = self.pool.get('physical.inventory.discrepancy')
572+ ignore_ids = [item['line_id'] for item in items if item['action'] == 'ignore']
573+ count_ids = [item['line_id'] for item in items if item['action'] == 'count']
574+
575+ if ignore_ids:
576+ discrepancies.write(cr, uid, ignore_ids, {'counted_qty': 0.0, 'ignored': True})
577+ if count_ids:
578+ discrepancies.write(cr, uid, count_ids, {'counted_qty': 0.0, 'ignored': False})
579+
580+ def get_stock_for_products_at_location(self, cr, uid, product_ids, location_id, context=None):
581+ context = context if context else {}
582+
583+ def read_many(model, ids, columns):
584+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
585+
586+ def search(model, domain):
587+ return self.pool.get(model).search(cr, uid, domain, context=context)
588+
589+ assert isinstance(product_ids, list)
590+ assert isinstance(location_id, int)
591+
592+ # Get all the moves for in/out of that location for the products
593+ move_for_products_at_location = ['&', '&', '|',
594+ ('location_id', 'in', [location_id]),
595+ ('location_dest_id', 'in', [location_id]),
596+ ("product_id", 'in', product_ids),
597+ ('state', '=', 'done')]
598+
599+ moves_at_location_ids = search("stock.move", move_for_products_at_location)
600+ moves_at_location = read_many("stock.move",
601+ moves_at_location_ids,
602+ ["product_id",
603+ "product_qty",
604+ "prodlot_id",
605+ "expired_date",
606+ "location_id",
607+ "location_dest_id"])
608+
609+ # Sum all lines to get a set of (product, batchnumber) -> qty
610+ stocks = {}
611+ for move in moves_at_location:
612+
613+ product_id = move["product_id"][0]
614+ product_qty = move["product_qty"]
615+ batch_number = move["prodlot_id"][1] if move["prodlot_id"] else False
616+ expiry_date = move["expired_date"]
617+
618+ # Dirty hack to ignore/hide internal batch numbers ("MSFBN")
619+ if batch_number and batch_number.startswith("MSFBN"):
620+ batch_number = False
621+
622+ product_batch_expirydate = (product_id, batch_number, expiry_date)
623+
624+ # Init the quantity to 0 if batch is not present in dict yet
625+ # (NB: batch_id can be None, but that's not an issue for dicts ;))
626+ if not product_batch_expirydate in stocks.keys():
627+ stocks[product_batch_expirydate] = 0.0
628+
629+ move_out = (move["location_id"][0] == location_id)
630+ move_in = (move["location_dest_id"][0] == location_id)
631+
632+ if move_in:
633+ stocks[product_batch_expirydate] += product_qty
634+ elif move_out:
635+ stocks[product_batch_expirydate] -= product_qty
636+ else:
637+ # This shouldnt happen
638+ pass
639+
640+ return stocks
641+
642+ def export_xls_counting_sheet(self, cr, uid, ids, context=None):
643+ return {
644+ 'type': 'ir.actions.report.xml',
645+ 'report_name': 'physical_inventory_counting_sheet_xls',
646+ 'datas': {'ids': ids, 'target_filename': 'counting_sheet'},
647+ 'nodestroy': True,
648+ 'context': context,
649+ }
650+
651+ def export_pdf_counting_sheet(self, cr, uid, ids, context=None):
652+ return {
653+ 'type': 'ir.actions.report.xml',
654+ 'report_name': 'physical_inventory_counting_sheet_pdf',
655+ 'datas': {'ids': ids, 'target_filename': 'counting_sheet'},
656+ 'nodestroy': True,
657+ 'context': context,
658+ }
659+
660+ def import_counting_sheet(self, cr, uid, ids, context=None):
661+ """
662+ Import an exported counting sheet
663+ """
664+ if not context:
665+ context = {}
666+
667+ counting_sheet_header = {}
668+ counting_sheet_lines = []
669+ counting_sheet_errors = []
670+
671+ def add_error(message, file_row, file_col=None):
672+ if file_col is not None:
673+ _msg = 'Cell %s%d: %s' % (chr(0x41 + file_col), file_row + 1, message)
674+ else:
675+ _msg = 'Line %d: %s' % (file_row + 1, message)
676+ counting_sheet_errors.append(_msg)
677+
678+ inventory_rec = self.browse(cr, uid, ids, context=context)[0]
679+ if not inventory_rec.file_to_import:
680+ raise osv.except_osv(_('Error'), _('Nothing to import.'))
681+ counting_sheet_file = SpreadsheetXML(xmlstring=base64.decodestring(inventory_rec.file_to_import))
682+
683+ product_obj = self.pool.get('product.product')
684+ product_uom_obj = self.pool.get('product.uom')
685+ counting_obj = self.pool.get('physical.inventory.counting')
686+
687+ line_list = []
688+ line_items = []
689+
690+ for row_index, row in enumerate(counting_sheet_file.getRows()):
691+
692+ # === Process header ===
693+
694+ if row_index == 2:
695+ counting_sheet_header.update({
696+ 'inventory_counter_name': row.cells[2].data, # Cell C3
697+ 'inventory_date': row.cells[5].data # Cell F3
698+ })
699+ elif row_index == 4:
700+ inventory_reference = row.cells[2].data # Cell C5
701+ inventory_location = row.cells[5].data # Cell F5
702+ # Check location
703+ if inventory_rec.location_id and inventory_rec.location_id.name != (inventory_location or '').strip():
704+ add_error(_('Location is different to inventory location'), row_index, 5)
705+
706+ # Check reference
707+ if inventory_rec.ref != (inventory_reference or '').strip():
708+ add_error(_('Reference is different to inventory reference'), row_index, 2)
709+ counting_sheet_header.update({
710+ 'location_id': inventory_rec.location_id,
711+ 'inventory_reference': inventory_reference
712+ })
713+ elif row_index == 6:
714+ counting_sheet_header['inventory_name'] = row.cells[2].data # Cell C7
715+ if row_index < 9:
716+ continue
717+
718+ # === Process lines ===
719+
720+ # Check number of columns
721+ if len(row) != 10:
722+ add_error(_("""_(Reference is different to inventory reference, You should have exactly 9 columns in this order:
723+Line #, Product Code*, Product Description*, UoM*, Quantity*, Batch*, Expiry Date*, Specification*, BN Management*, , ED Management*"""), row_index)
724+ break
725+
726+ # Check line number
727+ line_no = row.cells[0].data
728+ if line_no is not None:
729+ try:
730+ line_no = int(line_no)
731+ if line_no in line_list:
732+ add_error("""Line number is duplicate. If you added a line, please keep the line number empty.""", row_index, 0)
733+ line_list.append(line_no)
734+ except ValueError:
735+ line_no = None
736+ add_error("""Invalid line number""", row_index, 0)
737+
738+ # Check product_code
739+ product_code = row.cells[1].data
740+ product_ids = product_obj.search(cr, uid, [('default_code', '=like', product_code)], context=context)
741+ product_id = False
742+ if len(product_ids) == 1:
743+ product_id = product_ids[0]
744+ else:
745+ add_error("""Product %s not found""" % product_code, row_index, 1)
746+
747+ # Check UoM
748+ product_uom_id = False
749+ product_uom = row.cells[3].data
750+ product_uom_ids = product_uom_obj.search(cr, uid, [('name', '=like', product_uom)])
751+ if len(product_uom_ids) == 1:
752+ product_uom_id = product_uom_ids[0]
753+ else:
754+ add_error("""UoM %s unknown""" % product_uom, row_index, 3)
755+
756+ # Check quantity
757+ quantity = row.cells[4].data
758+ try:
759+ quantity = counting_obj.quantity_validate(cr, quantity)
760+ except NegativeValueError:
761+ add_error('Quantity %s is negative' % quantity, row_index, 4)
762+ quantity = 0.0
763+ except ValueError:
764+ quantity = 0.0
765+ add_error('Quantity %s is not valide' % quantity, row_index, 4)
766+
767+ product_info = product_obj.read(cr, uid, product_id, ['batch_management', 'perishable'])
768+
769+ # Check batch number
770+ batch_name = row.cells[5].data
771+ if not batch_name and product_info['batch_management'] and float(quantity or 0) > 0:
772+ add_error('Batch number is required', row_index, 5)
773+
774+ # Check expiry date
775+ expiry_date = row.cells[6].data
776+ if expiry_date:
777+ expiry_date_type = row.cells[6].type
778+ try:
779+ if expiry_date_type == 'datetime':
780+ expiry_date = expiry_date.strftime(DEFAULT_SERVER_DATE_FORMAT)
781+ elif expiry_date_type == 'str':
782+ expiry_date = parse(expiry_date).strftime(DEFAULT_SERVER_DATE_FORMAT)
783+ else:
784+ raise ValueError()
785+ except ValueError:
786+ add_error("""Expiry date %s is not valide""" % expiry_date, row_index, 6)
787+ if not expiry_date and product_info['perishable'] and float(quantity or 0) > 0:
788+ add_error('Expiry date is required', row_index, 6)
789+
790+ # Check duplicate line (Same product_id, batch_number, expirty_date)
791+ item = '%d-%s-%s' % (product_id or -1, batch_name or '', expiry_date or '')
792+ if item in line_items:
793+ add_error("""Duplicate line (same product, batch number and expiry date)""", row_index)
794+ else:
795+ line_items.append(item)
796+
797+ data = {
798+ 'line_no': line_no,
799+ 'product_id': product_id,
800+ 'batch_number': batch_name,
801+ 'expiry_date': expiry_date,
802+ 'quantity': quantity,
803+ 'product_uom_id': product_uom_id,
804+ }
805+
806+ # Check if line exist
807+ if line_no:
808+ line_ids = counting_obj.search(cr, uid, [('inventory_id', '=', inventory_rec.id), ('line_no', '=', line_no)])
809+ else:
810+ line_ids = counting_obj.search(cr, uid, [('inventory_id', '=', inventory_rec.id),
811+ ('product_id', '=', product_id),
812+ ('batch_number', '=', batch_name),
813+ ('expiry_date', '=', expiry_date)])
814+ if line_ids:
815+ del data["line_no"]
816+
817+ if len(line_ids) > 0:
818+ counting_sheet_lines.append((1, line_ids[0], data))
819+ else:
820+ counting_sheet_lines.append((0, 0, data))
821+
822+ # endfor
823+
824+ context['import_in_progress'] = True
825+ wizard_obj = self.pool.get('physical.inventory.import.wizard')
826+ if counting_sheet_errors:
827+ # Errors found, open message box for exlain
828+ self.write(cr, uid, ids, {'file_to_import': False}, context=context)
829+ result = wizard_obj.message_box(cr, uid, title='Importation errors', message='\n'.join(counting_sheet_errors))
830+ else:
831+ # No error found. Write counting lines on Inventory
832+ vals = {
833+ 'file_to_import': False,
834+ 'responsible': counting_sheet_header.get('inventory_counter_name'),
835+ 'counting_line_ids': counting_sheet_lines
836+ }
837+ self.write(cr, uid, ids, vals, context=context)
838+ result = wizard_obj.message_box(cr, uid, title='Information', message='Counting sheet succefully imported.')
839+ context['import_in_progress'] = False
840+
841+ return result
842+
843+ def import_xls_discrepancy_report(self, cr, uid, ids, context=None):
844+ """Import an exported discrepancy report"""
845+ if not context:
846+ context = {}
847+
848+ discrepancy_report_header = {}
849+ discrepancy_report_lines = []
850+ discrepancy_report_errors = []
851+
852+ def add_error(message, file_row, file_col):
853+ discrepancy_report_errors.append('Cell %s%d: %s' % (chr(0x41 + file_col), file_row + 1, message))
854+
855+ inventory_rec = self.browse(cr, uid, ids, context=context)[0]
856+ if not inventory_rec.file_to_import2:
857+ raise osv.except_osv(_('Error'), _('Nothing to import.'))
858+
859+ discrepancy_report_file = SpreadsheetXML(xmlstring=base64.decodestring(inventory_rec.file_to_import2))
860+
861+ # product_obj = self.pool.get('product.product')
862+ # product_uom_obj = self.pool.get('product.uom')
863+ # counting_obj = self.pool.get('physical.inventory.counting')
864+ reason_type_obj = self.pool.get('stock.reason.type')
865+ discrepancy_obj = self.pool.get('physical.inventory.discrepancy')
866+
867+ for row_index, row in enumerate(discrepancy_report_file.getRows()):
868+ if row_index < 10:
869+ continue
870+ adjustment_type = row.cells[18].data
871+ if adjustment_type:
872+ reason_ids = reason_type_obj.search(cr, uid, [('name', '=like', adjustment_type)], context=context)
873+ if reason_ids:
874+ adjustment_type = reason_ids[0]
875+ else:
876+ add_error('Unknown adjustment type %s' % adjustment_type, row_index, 18)
877+ adjustment_type = False
878+
879+ comment = row.cells[19].data
880+
881+ line_no = row.cells[0].data
882+ line_ids = discrepancy_obj.search(cr, uid, [('inventory_id', '=', inventory_rec.id), ('line_no', '=', line_no)])
883+ if line_ids:
884+ line_no = line_ids[0]
885+ else:
886+ add_error('Unknown line no %s' % line_no, row_index, 0)
887+ line_no = False
888+
889+ discrepancy_report_lines.append((1, line_no, {'reason_type_id': adjustment_type, 'comment': comment}))
890+ # endfor
891+
892+ context['import_in_progress'] = True
893+ wizard_obj = self.pool.get('physical.inventory.import.wizard')
894+ if discrepancy_report_errors:
895+ # Errors found, open message box for exlain
896+ self.write(cr, uid, ids, {'file_to_import2': False}, context=context)
897+ result = wizard_obj.message_box(cr, uid, title='Importation errors',
898+ message='\n'.join(discrepancy_report_errors))
899+ else:
900+ # No error found. update comment and reason for discrepancies lines on Inventory
901+ vals = {'file_to_import2': False, 'discrepancy_line_ids': discrepancy_report_lines}
902+ self.write(cr, uid, ids, vals, context=context)
903+ result = wizard_obj.message_box(cr, uid, title='Information',
904+ message='Discrepancy report succefully imported.')
905+ context['import_in_progress'] = False
906+
907+ return result
908+
909+ def export_xls_discrepancy_report(self, cr, uid, ids, context=None):
910+ return {
911+ 'type': 'ir.actions.report.xml',
912+ 'report_name': 'physical_inventory_discrepancies_report_xls',
913+ 'datas': {'ids': ids, 'target_filename': 'discrepancies'},
914+ 'nodestroy': True,
915+ 'context': context,
916+ }
917+
918+ def action_counted(self, cr, uid, ids, context=None):
919+ if context is None:
920+ context = {}
921+ self.write(cr, uid, ids, {'state': 'counted'}, context=context)
922+ return {}
923+
924+ def action_done(self, cr, uid, ids, context=None):
925+ """ Finish the inventory"""
926+ if context is None:
927+ context = {}
928+ move_obj = self.pool.get('stock.move')
929+ for inv in self.read(cr, uid, ids, ['move_ids'], context=context):
930+ move_obj.action_done(cr, uid, inv['move_ids'], context=context)
931+ self.write(cr, uid, ids, {'state': 'closed', 'date_done': time.strftime(DEFAULT_SERVER_DATETIME_FORMAT)},
932+ context=context)
933+ return {}
934+
935+ def action_recount(self, cr, uid, ids, context=None):
936+ if context is None:
937+ context = {}
938+ self.write(cr, uid, ids, {'state': 'counting'}, context=context)
939+ return {}
940+
941+ def action_validate(self, cr, uid, ids, context=None):
942+ if context is None:
943+ context = {}
944+ self.write(cr, uid, ids, {'state': 'validated'}, context=context)
945+ return {}
946+
947+ def action_confirm(self, cr, uid, ids, context=None):
948+ """ Confirm the inventory and writes its finished date"""
949+
950+ if context is None:
951+ context = {}
952+
953+ # to perform the correct inventory corrections we need analyze stock location by
954+ # location, never recursively, so we use a special context
955+ product_context = dict(context, compute_child=False)
956+
957+ # location_obj = self.pool.get('stock.location')
958+ product_obj = self.pool.get('product.product')
959+ product_tmpl_obj = self.pool.get('product.template')
960+ prod_lot_obj = self.pool.get('stock.production.lot')
961+ picking_obj = self.pool.get('stock.picking')
962+
963+ product_dict = {}
964+ product_tmpl_dict = {}
965+
966+ for inv in self.read(cr, uid, ids, ['counting_line_ids',
967+ 'discrepancy_line_ids',
968+ 'date',
969+ 'name'], context=context):
970+ move_ids = []
971+
972+ # gather all information needed for the lines treatment first to do less requests
973+ counting_line_obj = self.pool.get('physical.inventory.counting')
974+ inv_line_obj = self.pool.get('physical.inventory.discrepancy')
975+
976+
977+ counting_lines_with_no_discrepancy_ids = counting_line_obj.search(cr, uid, [("inventory_id", '=', inv["id"]),
978+ ("discrepancy", '=', False)],
979+ context=context)
980+
981+ counting_lines_with_no_discrepancy = counting_line_obj.read(cr, uid, counting_lines_with_no_discrepancy_ids,
982+ ['product_id',
983+ 'batch_number',
984+ 'expiry_date'],
985+ context=context)
986+
987+ line_read = inv_line_obj.read(cr, uid, inv['discrepancy_line_ids'],
988+ ['inventory_id', 'product_id', 'product_uom_id', 'batch_number', 'expiry_date', 'location_id',
989+ 'discrepancy_qty', 'reason_type_id', 'comment', 'ignored', 'line_no'], context=context)
990+
991+ product_id_list = [x['product_id'][0] for x in line_read if
992+ x['product_id'][0] not in product_dict]
993+ product_id_list = list(set(product_id_list))
994+ product_read = product_obj.read(cr, uid, product_id_list,
995+ ['product_tmpl_id'], context=context)
996+ for product in product_read:
997+ product_id = product['id']
998+ product_dict[product_id] = {}
999+ product_dict[product_id]['p_tmpl_id'] = product['product_tmpl_id'][0]
1000+
1001+ tmpl_ids = [x['p_tmpl_id'] for x in product_dict.values()]
1002+
1003+ product_tmpl_id_list = [x for x in tmpl_ids if x not in product_tmpl_dict]
1004+ product_tmpl_id_list = list(set(product_tmpl_id_list))
1005+ product_tmpl_read = product_tmpl_obj.read(cr, uid,
1006+ product_tmpl_id_list, ['property_stock_inventory'],
1007+ context=context)
1008+ product_tmpl_dict = dict((x['id'], x['property_stock_inventory'][0]) for x in product_tmpl_read)
1009+
1010+ for product_id in product_id_list:
1011+ product_tmpl_id = product_dict[product_id]['p_tmpl_id']
1012+ stock_inventory = product_tmpl_dict[product_tmpl_id]
1013+ product_dict[product_id]['stock_inventory'] = stock_inventory
1014+
1015+ errors = []
1016+ for line in line_read:
1017+ if not line['ignored'] and not line['reason_type_id']:
1018+ errors.append('Line %d: Adjustement type missing' % line['line_no'])
1019+
1020+ if errors:
1021+ # Errors found, open message box for exlain
1022+ wizard_obj = self.pool.get('physical.inventory.import.wizard')
1023+ return wizard_obj.message_box(cr, uid, title='Confirmation errors', message='\n'.join(errors))
1024+
1025+
1026+ def get_prodlot_id(bn, ed):
1027+ if not ed:
1028+ return False
1029+ elif bn:
1030+ return picking_obj.retrieve_batch_number(cr, uid, pid, {'name': bn, 'life_date': ed}, context=context)[0]
1031+ else:
1032+ return prod_lot_obj._get_prodlot_from_expiry_date(cr, uid, ed, pid)
1033+
1034+ # For each counting lines which had no discrepancy, keep track of
1035+ # the real batch number id
1036+ for line in counting_lines_with_no_discrepancy:
1037+ line_id = line['id']
1038+ pid = line['product_id'][0]
1039+ bn = line['batch_number']
1040+ ed = line['expiry_date']
1041+
1042+ lot_id = get_prodlot_id(bn, ed)
1043+
1044+ counting_line_obj.write(cr, uid, [line_id], {"prod_lot_id": lot_id}, context=context)
1045+
1046+
1047+ discrepancy_to_move = {}
1048+ for line in line_read:
1049+ if line['ignored']:
1050+ continue
1051+ line_id = line['id']
1052+ pid = line['product_id'][0]
1053+ bn = line['batch_number']
1054+ ed = line['expiry_date']
1055+
1056+ change = line['discrepancy_qty'] # - amount
1057+
1058+ # Ignore lines with no discrepancies (there shouldnt be any
1059+ # by definition/construction of the discrepancy lines)
1060+ if not change:
1061+ continue
1062+
1063+ lot_id = get_prodlot_id(bn, ed)
1064+
1065+ # lot_id = line['prod_lot_id'] and line['prod_lot_id'][0] or False
1066+ product_context.update(uom=line['product_uom_id'][0],
1067+ date=inv['date'], prodlot_id=lot_id)
1068+
1069+ # amount = location_obj._product_get(cr, uid, line['location_id'][0], [pid], product_context)[pid]
1070+
1071+ location_id = product_dict[line['product_id'][0]]['stock_inventory']
1072+ value = {
1073+ 'name': 'INV:' + str(inv['id']) + ':' + inv['name'],
1074+ 'product_id': line['product_id'][0],
1075+ 'product_uom': line['product_uom_id'][0],
1076+ 'prodlot_id': lot_id,
1077+ 'date': inv['date'],
1078+ }
1079+ if change > 0:
1080+ value.update({
1081+ 'product_qty': change,
1082+ 'location_id': location_id,
1083+ 'location_dest_id': line['location_id'][0],
1084+ })
1085+ else:
1086+ value.update({
1087+ 'product_qty': -change,
1088+ 'location_id': line['location_id'][0],
1089+ 'location_dest_id': location_id,
1090+ })
1091+ value.update({
1092+ 'comment': line['comment'],
1093+ 'reason_type_id': line['reason_type_id'][0],
1094+ })
1095+ move_id = self.pool.get('stock.move').create(cr, uid, value)
1096+ move_ids.append(move_id)
1097+ discrepancy_to_move[line_id] = move_id
1098+
1099+ message = _('Inventory') + " '" + inv['name'] + "' " + _("is validated.")
1100+ self.log(cr, uid, inv['id'], message)
1101+ self.write(cr, uid, [inv['id']], {'state': 'confirmed', 'move_ids': [(6, 0, move_ids)]})
1102+ for line_id, move_id in discrepancy_to_move.items():
1103+ inv_line_obj.write(cr, uid, [line_id], {'move_id': move_id}, context=context)
1104+
1105+
1106+ def action_cancel_draft(self, cr, uid, ids, context=None):
1107+ """ Cancels the stock move and change inventory state to draft."""
1108+ for inv in self.read(cr, uid, ids, ['move_ids'], context=context):
1109+ self.pool.get('stock.move').action_cancel(cr, uid, inv['move_ids'], context=context)
1110+ self.write(cr, uid, ids, {'state': 'draft'}, context=context)
1111+ return {}
1112+
1113+ def action_cancel_inventary(self, cr, uid, ids, context=None):
1114+ """ Cancels both stock move and inventory"""
1115+ move_obj = self.pool.get('stock.move')
1116+ account_move_obj = self.pool.get('account.move')
1117+ for inv in self.browse(cr, uid, ids, context=context):
1118+ move_obj.action_cancel(cr, uid, [x.id for x in inv.move_ids], context=context)
1119+ for move in inv.move_ids:
1120+ account_move_ids = account_move_obj.search(cr, uid, [('name', '=', move.name)], order='NO_ORDER')
1121+ if account_move_ids:
1122+ account_move_data_l = account_move_obj.read(cr, uid, account_move_ids, ['state'], context=context)
1123+ for account_move in account_move_data_l:
1124+ if account_move['state'] == 'posted':
1125+ raise osv.except_osv(_('UserError'),
1126+ _('You can not cancel inventory which has any account move with posted state.'))
1127+ account_move_obj.unlink(cr, uid, [account_move['id']], context=context)
1128+ self.write(cr, uid, [inv.id], {'state': 'cancel'}, context=context)
1129+ self.infolog(cr, uid, "The Physical inventory id:%s (%s) has been cancelled" % (inv.id, inv.name))
1130+ return {}
1131+
1132+
1133+PhysicalInventory()
1134+
1135+
1136+class PhysicalInventoryCounting(osv.osv):
1137+ _name = 'physical.inventory.counting'
1138+ _description = 'Physical Inventory Counting Line'
1139+
1140+ _columns = {
1141+ # Link to inventory
1142+ 'inventory_id': fields.many2one('physical.inventory', _('Inventory'), ondelete='cascade', select=True),
1143+
1144+ # Product
1145+ 'product_id': fields.many2one('product.product', _('Product'), required=True, select=True,
1146+ domain=[('type', '<>', 'service')]),
1147+ 'product_uom_id': fields.many2one('product.uom', _('Product UOM'), required=True),
1148+ 'standard_price': fields.float(_("Unit Price"), readonly=True),
1149+ 'currency_id': fields.many2one('res.currency', "Currency", readonly=True),
1150+ 'is_bn': fields.related('product_id', 'batch_management', string='BN', type='boolean', readonly=True),
1151+ 'is_ed': fields.related('product_id', 'perishable', string='ED', type='boolean', readonly=True),
1152+ 'is_kc': fields.related('product_id', 'is_kc', string='KC', type='boolean', readonly=True),
1153+ 'is_dg': fields.related('product_id', 'is_dg', string='DG', type='boolean', readonly=True),
1154+ 'is_cs': fields.related('product_id', 'is_cs', string='CS', type='boolean', readonly=True),
1155+
1156+ # Batch / Expiry date
1157+ 'batch_number': fields.char(_('Batch number'), size=30),
1158+ 'expiry_date': fields.date(string=_('Expiry date')),
1159+
1160+ # Specific to inventory
1161+ 'line_no': fields.integer(string=_('Line #'), readonly=True),
1162+ 'quantity': fields.char(_('Quantity'), size=15),
1163+ 'discrepancy': fields.boolean('Discrepancy found', readonly=True),
1164+
1165+ # Actual batch number id, filled after the inventory switches to done
1166+ 'prod_lot_id': fields.many2one('stock.production.lot', 'Production Lot', readonly=True)
1167+ }
1168+
1169+ _sql_constraints = [
1170+ ('line_uniq', 'UNIQUE(inventory_id, product_id, batch_number, expiry_date)', _('The line product, batch number and expiry date must be unique!')),
1171+ ]
1172+
1173+ def create(self, cr, user, vals, context=None):
1174+ # Compute line number
1175+ if not vals.get('line_no'):
1176+ cr.execute("""SELECT MAX(line_no) FROM physical_inventory_counting WHERE inventory_id=%s""",
1177+ (vals.get('inventory_id'),))
1178+ vals['line_no'] = (cr.fetchone()[0] or 0) + 1 # Last line number + 1
1179+
1180+ if (not vals.get('product_uom_id')
1181+ or not vals.get('standard_price')
1182+ or not vals.get('currency_id')):
1183+
1184+ product_id = vals.get('product_id')
1185+ product = self.pool.get("product.product").read(cr, user,
1186+ [product_id],
1187+ ["uom_id",
1188+ "standard_price",
1189+ "currency_id"],
1190+ context=context)[0]
1191+
1192+ vals['product_uom_id'] = product['uom_id'][0]
1193+ vals['standard_price'] = product['standard_price']
1194+ vals['currency_id'] = product['currency_id'][0]
1195+
1196+ return super(PhysicalInventoryCounting, self).create(cr, user, vals, context)
1197+
1198+ @staticmethod
1199+ def quantity_validate(cr, quantity):
1200+ """Return a valide quantity or raise ValueError exception"""
1201+ if quantity:
1202+ float_width, float_prec = dp.get_precision('Product UoM')(cr)
1203+ quantity = float(quantity)
1204+ if quantity < 0:
1205+ raise NegativeValueError()
1206+ if math.isnan(quantity):
1207+ raise ValueError()
1208+ quantity = '%.*f' % (float_prec, quantity)
1209+ return quantity
1210+
1211+ def on_change_quantity(self, cr, uid, ids, quantity):
1212+ """Check and format quantity."""
1213+ if quantity:
1214+ try:
1215+ quantity = self.quantity_validate(cr, quantity)
1216+ except NegativeValueError:
1217+ return {'value': {'quantity': False},
1218+ 'warning': {'title': 'warning', 'message': 'Negative quantity is not permit.'}}
1219+ except ValueError:
1220+ return {'value': {'quantity': False},
1221+ 'warning': {'title': 'warning', 'message': 'Enter a valid quantity.'}}
1222+ return {'value': {'quantity': quantity}}
1223+
1224+ def on_change_product_id(self, cr, uid, ids, product_id, uom=False):
1225+ """Changes UoM and quantity if product_id changes."""
1226+ if product_id and not uom:
1227+ product_rec = self.pool.get('product.product').browse(cr, uid, product_id)
1228+ uom = product_rec.uom_id and product_rec.uom_id.id
1229+ return {'value': {'quantity': False, 'product_uom_id': product_id and uom}}
1230+
1231+ def perm_write(self, cr, user, ids, fields, context=None):
1232+ pass
1233+
1234+
1235+PhysicalInventoryCounting()
1236+
1237+
1238+class PhysicalInventoryDiscrepancy(osv.osv):
1239+ _name = 'physical.inventory.discrepancy'
1240+ _description = 'Physical Inventory Discrepancy Line'
1241+
1242+
1243+ def _discrepancy(self, cr, uid, ids, field_names, arg, context=None):
1244+ context = context is None and {} or context
1245+ def read_many(model, ids, columns):
1246+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
1247+
1248+ lines = read_many("physical.inventory.discrepancy", ids, ["theoretical_qty",
1249+ "counted_qty",
1250+ "standard_price"])
1251+
1252+ discrepancies = {}
1253+ for line in lines:
1254+ discrepancy_qty = line["counted_qty"] - line["theoretical_qty"]
1255+ discrepancy_value = discrepancy_qty * line["standard_price"]
1256+ discrepancies[line["id"]] = { "discrepancy_qty": discrepancy_qty,
1257+ "discrepancy_value": discrepancy_value }
1258+
1259+ return discrepancies
1260+
1261+
1262+ def _total_product_qty_and_values(self, cr, uid, ids, field_names, arg, context=None):
1263+ def search(model, domain):
1264+ return self.pool.get(model).search(cr, uid, domain, context=context)
1265+ def read_many(model, ids, columns):
1266+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
1267+
1268+ discrepancy_lines = read_many(self._name, ids, ["product_id",
1269+ "standard_price",
1270+ "total_product_theoretical_qty",
1271+ "total_product_counted_qty" ])
1272+
1273+ total_product_qty_and_values = {}
1274+ for line in discrepancy_lines:
1275+ id_ = line["id"]
1276+ theo = line["total_product_theoretical_qty"]
1277+ counted = line["total_product_counted_qty"]
1278+ price = line["standard_price"]
1279+ total_product_qty_and_values[id_] = {
1280+ 'total_product_counted_value': counted * price,
1281+ 'total_product_discrepancy_qty': counted - theo,
1282+ 'total_product_discrepancy_value': (counted - theo) * price
1283+ }
1284+
1285+ return total_product_qty_and_values
1286+
1287+
1288+ _columns = {
1289+ # Link to inventory
1290+ 'inventory_id': fields.many2one('physical.inventory', 'Inventory', ondelete='cascade'),
1291+ 'location_id': fields.related('inventory_id', 'location_id', type='many2one', relation='stock.location', string='location_id', readonly=True),
1292+
1293+ # Product
1294+ 'product_id': fields.many2one('product.product', 'Product', required=True),
1295+
1296+ 'product_uom_id': fields.many2one('product.uom', 'UOM', required=True, readonly=True),
1297+
1298+ 'nomen_manda_2': fields.related('product_id', 'nomen_manda_2', string="Family",
1299+ relation="product.nomenclature", type='many2one', readonly=True),
1300+
1301+ 'standard_price': fields.float(_("Unit Price"), readonly=True),
1302+ 'currency_id': fields.many2one('res.currency', "Currency", readonly=True),
1303+
1304+ # BN / ED
1305+ 'batch_number': fields.char(_('Batch number'), size=30, readonly=True),
1306+ 'expiry_date': fields.date(string=_('Expiry date')),
1307+
1308+ # Count
1309+ 'line_no': fields.integer(string=_('Line #'), readonly=True),
1310+ 'theoretical_qty': fields.float('Theoretical Quantity', digits_compute=dp.get_precision('Product UoM'), readonly=True),
1311+ 'counted_qty': fields.float('Counted Quantity', digits_compute=dp.get_precision('Product UoM')),
1312+ 'discrepancy_qty': fields.function(_discrepancy, multi="discrepancy", method=True, type='float', string=_("Discrepancy Quantity")),
1313+ 'discrepancy_value': fields.function(_discrepancy, multi="discrepancy", method=True, type='float', string=_("Discrepancy Value")),
1314+
1315+ # Discrepancy analysis
1316+ 'reason_type_id': fields.many2one('stock.reason.type', string='Adjustment type', select=True),
1317+ 'comment': fields.char(size=128, string='Comment'),
1318+
1319+ # Total for product
1320+ 'total_product_theoretical_qty': fields.float('Total Theoretical Quantity for product', digits_compute=dp.get_precision('Product UoM'), readonly=True),
1321+ 'total_product_counted_qty': fields.float('Total Counted Quantity for product', digits_compute=dp.get_precision('Product UoM'), readonly=True),
1322+ 'total_product_counted_value': fields.function(_total_product_qty_and_values, multi="total_product", method=True, type='float', string=_("Total Counted Value for product")),
1323+ 'total_product_discrepancy_qty': fields.function(_total_product_qty_and_values, multi="total_product", method=True, type='float', string=_("Total Discrepancy for product")),
1324+ 'total_product_discrepancy_value': fields.function(_total_product_qty_and_values, multi="total_product", method=True, type='float', string=_("Total Discrepancy Value for product")),
1325+ 'ignored': fields.boolean('Ignored', readonly=True),
1326+ 'move_id': fields.integer(readonly=True)
1327+ }
1328+
1329+ _order = "product_id asc, line_no asc"
1330+
1331+ def create(self, cr, user, vals, context=None):
1332+ context = context is None and {} or context
1333+
1334+ if (not vals.get('product_uom_id')
1335+ or not vals.get('standard_price')
1336+ or not vals.get('currency_id')):
1337+
1338+ product_id = vals.get('product_id')
1339+ product = self.pool.get("product.product").read(cr, user,
1340+ [product_id],
1341+ ["uom_id",
1342+ "standard_price",
1343+ "currency_id"],
1344+ context=context)[0]
1345+
1346+ vals['product_uom_id'] = product['uom_id'][0]
1347+ vals['standard_price'] = product['standard_price']
1348+ vals['currency_id'] = product['currency_id'][0]
1349+
1350+ return super(PhysicalInventoryDiscrepancy, self).create(cr, user, vals, context)
1351+
1352+ def write(self, cr, uid, ids, vals, context=None):
1353+ context = context is None and {} or context
1354+
1355+ r = super(PhysicalInventoryDiscrepancy, self).write(cr, uid, ids, vals, context=context)
1356+ move_obj = self.pool.get("stock.move")
1357+
1358+ lines = self.read(cr, uid, ids, ["move_id", "comment"], context=context)
1359+
1360+ for line in lines:
1361+ if not line["move_id"]:
1362+ continue
1363+ reason_type_id = vals.get("reason_type_id", False)
1364+ comment = vals.get("comment", False)
1365+ to_update = {}
1366+ if reason_type_id:
1367+ to_update["reason_type_id"] = reason_type_id
1368+ if comment:
1369+ to_update["comment"] = comment
1370+ if to_update:
1371+ move_obj.write(cr, uid, [line["move_id"]], to_update, context=context)
1372+
1373+ return r
1374+
1375+
1376+ def perm_write(self, cr, user, ids, fields, context=None):
1377+ pass
1378+
1379+
1380+PhysicalInventoryDiscrepancy()
1381
1382=== added file 'bin/addons/stock/physical_inventory_data.xml'
1383--- bin/addons/stock/physical_inventory_data.xml 1970-01-01 00:00:00 +0000
1384+++ bin/addons/stock/physical_inventory_data.xml 2017-11-24 09:38:22 +0000
1385@@ -0,0 +1,17 @@
1386+<?xml version="1.0" encoding="utf-8"?>
1387+<openerp>
1388+ <data noupdate="1">
1389+ <!-- Sequences for physical.inventory -->
1390+ <record id="seq_type_physical_inventory" model="ir.sequence.type">
1391+ <field name="name">Physical inventory</field>
1392+ <field name="code">physical.inventory</field>
1393+ </record>
1394+ <record id="seq_physical_inventory" model="ir.sequence">
1395+ <field name="name">Physical inventory</field>
1396+ <field name="code">physical.inventory</field>
1397+ <field name="prefix">%(y)s/%(hqcode)s/%(instance)s/PI</field>
1398+ <field name="padding">5</field>
1399+ <field name="implementation">psql</field>
1400+ </record>
1401+ </data>
1402+</openerp>
1403\ No newline at end of file
1404
1405=== added file 'bin/addons/stock/physical_inventory_view.xml'
1406--- bin/addons/stock/physical_inventory_view.xml 1970-01-01 00:00:00 +0000
1407+++ bin/addons/stock/physical_inventory_view.xml 2017-11-24 09:38:22 +0000
1408@@ -0,0 +1,267 @@
1409+<?xml version="1.0" encoding="utf-8"?>
1410+<openerp>
1411+ <data>
1412+ <!-- Formulary -->
1413+ <record id="view_physical_inventory_form" model="ir.ui.view">
1414+ <field name="name">physical.inventory.form</field>
1415+ <field name="model">physical.inventory</field>
1416+ <field name="type">form</field>
1417+ <field name="arch" type="xml">
1418+ <form string="Physical Inventory" hide_delete_button="1">
1419+ <field name="name" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
1420+ <field name="ref"/>
1421+ <field name="location_id" domain="[('usage','=','internal')]"
1422+ attrs="{'readonly': [('state', '!=', 'draft')]}"/>
1423+ <field name="date" attrs="{'readonly': [('state', '!=', 'draft')]}"/>
1424+ <field name="responsible"/>
1425+ <field name="date_done"/>
1426+ <field name="full_inventory"/>
1427+ <button colspan="2" name="set_full_inventory" states="draft" string="Set as full inventory" type="object"
1428+ icon="gtk-about"
1429+ confirm="Setting as 'full inventory' is irreversible. Do you confirm ?"
1430+ attrs="{'invisible': [('full_inventory', '=', True)]}"/>
1431+ <notebook colspan="4">
1432+ <page string="Products" attrs="{'readonly': [('state', 'not in', ['draft'])]}">
1433+ <button name="action_select_products" states="draft" string="Products Selection" type="object"
1434+ icon="gtk-add"/>
1435+ <field colspan="4" name="product_ids" nolabel="1"
1436+ attrs="{'readonly': ['|', ('full_inventory', '=', True), ('state', 'not in', ['draft'])]}">
1437+ <tree string="Products" noteditable="1">
1438+ <field name="default_code"/>
1439+ <field name="name"/>
1440+ <field name="uom_id"/>
1441+ <field name="batch_management" string="BN mandatory"/>
1442+ <field name="perishable" string="ED mandatory"/>
1443+ <field name="is_kc" string="KC"/>
1444+ <field name="is_dg" string="DG"/>
1445+ <field name="is_cs" string="CS"/>
1446+ </tree>
1447+ </field>
1448+ </page>
1449+ <page string="Counting sheet" attrs="{'invisible': [('state', 'in', ['draft'])], 'readonly': [('state', 'not in', ['counting', 'counted', 'validated'])]}">
1450+ <group col="4" colspan="4"
1451+ attrs="{'invisible': [('state', 'not in', ['counting', 'counted', 'validated'])]}">
1452+ <button name="export_pdf_counting_sheet" string="Export pdf counting sheet"
1453+ type="object" colspan="2" icon="gtk-print"/>
1454+ <button name="export_xls_counting_sheet" string="Export xls counting sheet"
1455+ type="object" colspan="2" icon="gtk-print"/>
1456+ <field name="file_to_import" colspan="2"/>
1457+ <button name="import_counting_sheet" string="Import xls counting sheet" type="object" colspan="2" icon="gtk-save"/>
1458+ </group>
1459+ <field colspan="4" name="counting_line_ids" nolabel="1" widget="one2many_list"
1460+ attrs="{'readonly': [('state', 'not in', ['counting', 'counted', 'validated'])]}">
1461+ <tree string="Counting lines" editable="bottom">
1462+ <field name="line_no"/>
1463+ <field name="product_id" on_change="on_change_product_id(product_id,product_uom)"/>
1464+ <field name="product_uom_id"/>
1465+ <field name="batch_number" attrs="{'readonly': [('is_bn', '=', False)],'required': [('is_bn', '=', True)] }"/>
1466+ <field name="expiry_date" attrs="{'readonly': [('is_ed', '=', False)],'required': [('is_ed', '=', True)] }"/>
1467+ <field name="quantity" on_change="on_change_quantity(quantity)"/>
1468+ <field name="is_bn" string="BN"/>
1469+ <field name="is_ed" string="ED"/>
1470+ <field name="is_kc" string="KC"/>
1471+ <field name="is_dg" string="DG"/>
1472+ <field name="is_cs" string="CS"/>
1473+ </tree>
1474+ <form string="Counting line">
1475+ <field name="line_no"/>
1476+ <field name="product_id" on_change="on_change_product_id(product_id,product_uom)"/>
1477+ <field name="product_uom_id"/>
1478+ <field name="batch_number" attrs="{'readonly': [('is_bn', '=', False)],'required': [('is_bn', '=', True)] }"/>
1479+ <field name="expiry_date" attrs="{'readonly': [('is_ed', '=', False)],'required': [('is_ed', '=', True)] }"/>
1480+ <field name="quantity" on_change="on_change_quantity(quantity)"/>
1481+ <field name="is_bn" string="BN"/>
1482+ <field name="is_ed" string="ED"/>
1483+ <field name="is_kc" string="KC"/>
1484+ <field name="is_dg" string="DG"/>
1485+ <field name="is_cs" string="CS"/>
1486+ </form>
1487+ </field>
1488+ </page>
1489+ <page string="Discrepancy report" attrs="{'invisible': [('state', 'in', ['draft','counting'])], 'readonly': [('state', 'not in', ['counted', 'validated', 'confirmed'])]}">
1490+ <group col="4" colspan="4">
1491+ <group col="4" colspan="4">
1492+ <label string="" />
1493+ <label string="Number of lines" />
1494+ <label string="Value" />
1495+ <label string="Absolute value" />
1496+ </group>
1497+ <group col="4" colspan="4">
1498+ <label string="Total at the end" />
1499+ <field name="inventory_lines_number" nolabel="1" />
1500+ <field name="inventory_lines_value" nolabel="1" />
1501+ <field name="inventory_lines_absvalue" nolabel="1" />
1502+ </group>
1503+ <group col="4" colspan="4">
1504+ <label string="With discrepancies" />
1505+ <field name="discrepancy_lines_number" nolabel="1" />
1506+ <field name="discrepancy_lines_value" nolabel="1" />
1507+ <field name="discrepancy_lines_absvalue" nolabel="1" />
1508+ </group>
1509+ <group col="4" colspan="4">
1510+ <label string="Percent" />
1511+ <field name="discrepancy_lines_percent" nolabel="1" />
1512+ <field name="discrepancy_lines_percent_value" nolabel="1" />
1513+ <field name="discrepancy_lines_percent_absvalue" nolabel="1" />
1514+ </group>
1515+ </group>
1516+ <group col="4" colspan="4" attrs="{'invisible': [('state', 'not in', ['counted', 'validated'])]}">
1517+ <button name="export_xls_discrepancy_report" string="Export XLS discrepancy report"
1518+ type="object" icon="gtk-print" colspan="4"/>
1519+ <field name="file_to_import2" colspan="2"/>
1520+ <button name="import_xls_discrepancy_report" string="Import XLS discrepancy report"
1521+ type="object" icon="gtk-save" colspan="2"/>
1522+ </group>
1523+ <field colspan="4" name="discrepancy_line_ids" nolabel="1" widget="one2many_list"
1524+ filter_selector="[('Hide ignored', ('ignored', '!=', True)), ('Show all', ''), ('Show ignored only', ('ignored', '=', True))]">
1525+ <tree string="Discrepancies" editable="bottom" hide_delete_button="True" hide_new_button="True"
1526+ colors="grey:ignored==True">
1527+ <field name="line_no" />
1528+ <field name="nomen_manda_2" />
1529+ <field name="product_id" />
1530+ <field name="product_uom_id" />
1531+ <field name="standard_price" />
1532+ <field name="currency_id" />
1533+ <field name="theoretical_qty" />
1534+ <field name="counted_qty" readonly="1" />
1535+ <field name="batch_number" />
1536+ <field name="expiry_date" />
1537+ <field name="discrepancy_qty" />
1538+ <field name="discrepancy_value" />
1539+ <field name="total_product_theoretical_qty" />
1540+ <field name="total_product_counted_qty" />
1541+ <field name="total_product_counted_value" />
1542+ <field name="total_product_discrepancy_qty" />
1543+ <field name="total_product_discrepancy_value" />
1544+ <field name="reason_type_id" widget="selection" domain="[('is_inventory', '=', True)]" />
1545+ <field name="comment" />
1546+ <field name="ignored" invisible="True"/>
1547+ </tree>
1548+ <form string="Discrepancy line">
1549+ <field name="line_no" />
1550+ <field name="nomen_manda_2" />
1551+ <field name="product_id" />
1552+ <field name="product_uom_id" />
1553+ <field name="standard_price" />
1554+ <field name="currency_id" />
1555+ <field name="theoretical_qty" />
1556+ <field name="counted_qty" readonly="1" />
1557+ <field name="batch_number" />
1558+ <field name="expiry_date" />
1559+ <field name="discrepancy_qty" />
1560+ <field name="discrepancy_value" />
1561+ <field name="total_product_theoretical_qty" />
1562+ <field name="total_product_counted_qty" />
1563+ <field name="total_product_counted_value" />
1564+ <field name="total_product_discrepancy_qty" />
1565+ <field name="total_product_discrepancy_value" />
1566+ <field name="reason_type_id" widget="selection" domain="[('is_inventory', '=', True)]" />
1567+ <field name="comment" />
1568+ <field name="ignored" invisible="True"/>
1569+ </form>
1570+ </field>
1571+ </page>
1572+ <page string="Posted Inventory" attrs="{'invisible': [('state', 'not in', ['confirmed', 'closed'])]}">
1573+ <field colspan="2" name="move_ids" nolabel="1" widget="one2many_list"
1574+ context="{'inventory_id':active_id}">
1575+ <tree string="Stock Moves">
1576+ <field name="product_id"/>
1577+ <field name="product_qty"
1578+ on_change="onchange_quantity(product_id, product_qty, product_uom, product_uos)"/>
1579+ <field name="product_uom" string="UoM"/>
1580+ <field name="prodlot_id"/>
1581+ <field name="reason_type_id" widget="selection"
1582+ domain="[('is_inventory', '=', True)]"/>
1583+ <field name="tracking_id"/>
1584+ <field name="location_id"/>
1585+ <field name="location_dest_id"/>
1586+ <field name="date" string="Date"/>
1587+ </tree>
1588+ </field>
1589+ </page>
1590+ </notebook>
1591+ <group col="2" colspan="2">
1592+ <field name="state"/>
1593+ </group>
1594+ <group colspan="2">
1595+ <button name="action_cancel_inventary" states="draft,counting,counted,validated" string="Cancel Inventory"
1596+ type="object" icon="gtk-cancel" confirm="Do you confirm that you want to cancel the inventory ?"/>
1597+
1598+ <button name="generate_counting_sheet" states="draft" string="Generate Counting Sheet"
1599+ type="object" icon="gtk-go-forward"/>
1600+
1601+ <button name="action_counted" states="counting" string="Finish Counting"
1602+ type="object" icon="gtk-paste-v"/>
1603+
1604+ <button name="generate_discrepancies" states="counted,validated" string="Generate discrepancies"
1605+ type="object" icon="gtk-refresh"/>
1606+
1607+ <button name="action_validate" states="counted" string="Finished"
1608+ type="object" icon="gtk-apply" confirm="Do you confirm that you have finished the inventory ?"/>
1609+
1610+ <button name="action_recount" states="validated" string="Recount"
1611+ type="object" icon="gtk-undo"/>
1612+ <button name="action_confirm" states="validated" string="Confirm Inventory"
1613+ type="object" icon="gtk-jump-to" confirm="Do you confirm that you want confirm the inventory ?"/>
1614+
1615+ <button name="action_done" states="confirmed" string="Close Inventory"
1616+ type="object" icon="gtk-jump-to" confirm="Do you confirm that you want to close the inventory ?"/>
1617+
1618+ <button name="action_cancel_draft" states="cancel" string="Set to Draft"
1619+ type="object" icon="gtk-convert"/>
1620+ </group>
1621+ </form>
1622+ </field>
1623+ </record>
1624+ <!-- List -->
1625+ <record id="view_physical_inventory_tree" model="ir.ui.view">
1626+ <field name="name">physical.inventory.tree</field>
1627+ <field name="model">physical.inventory</field>
1628+ <field name="type">tree</field>
1629+ <field name="arch" type="xml">
1630+ <tree string="Physical Inventories" colors="grey:state in ('cancel')">
1631+ <field name="ref"/>
1632+ <field name="name"/>
1633+ <field name="location_id"/>
1634+ <field name="date"/>
1635+ <field name="state"/>
1636+ </tree>
1637+ </field>
1638+ </record>
1639+ <!-- Search -->
1640+ <record id="view_physical_inventory_search" model="ir.ui.view">
1641+ <field name="name">physical.inventory.search</field>
1642+ <field name="model">physical.inventory</field>
1643+ <field name="type">search</field>
1644+ <field name="arch" type="xml">
1645+ <search string="Search Physical Inventory">
1646+ <group col="10" colspan="4">
1647+ <field name="name"/>
1648+ <field name="date"/>
1649+ <field name="location_id" domain="[('usage','=','internal')]"/>
1650+ <field name="company_id" groups="base.group_multi_company" widget="selection"/>
1651+ </group>
1652+ <newline/>
1653+ <group expand="0" string="Group By..." colspan="4" col="4" groups="base.group_extended">
1654+ <filter string="State" icon="terp-stock_effects-object-colorize" domain="[]"
1655+ context="{'group_by':'state'}"/>
1656+ <filter string="Date" icon="terp-go-month" domain="[]" context="{'group_by':'date'}"/>
1657+ </group>
1658+ </search>
1659+ </field>
1660+ </record>
1661+ <!-- Action -->
1662+ <record id="action_physical_inventory" model="ir.actions.act_window">
1663+ <field name="name">Physical Inventories</field>
1664+ <field name="type">ir.actions.act_window</field>
1665+ <field name="res_model">physical.inventory</field>
1666+ <field name="view_type">form</field>
1667+ <field name="view_id" ref="view_physical_inventory_tree"/>
1668+ <field name="context">{'full':'1'}</field>
1669+ <field name="search_view_id" ref="view_physical_inventory_search" />
1670+ <field name="help">Periodical Inventories are used to count the number of products available per location. You can use it once a year when you do the general inventory or whenever you need it, to correct the current stock level of a product.</field>
1671+ </record>
1672+ <!-- Menu -->
1673+ <menuitem action="action_physical_inventory" id="menu_action_physical_inventory" parent="menu_stock_inventory_control" sequence="23"/>
1674+ </data>
1675+</openerp>
1676
1677=== modified file 'bin/addons/stock/report/__init__.py'
1678--- bin/addons/stock/report/__init__.py 2011-01-14 00:11:01 +0000
1679+++ bin/addons/stock/report/__init__.py 2017-11-24 09:38:22 +0000
1680@@ -27,4 +27,5 @@
1681 import report_stock_move
1682 import stock_inventory_move_report
1683 import lot_overview
1684-
1685+import physical_inventory_counting_sheet
1686+import physical_inventory_discrepancies_report
1687
1688=== added file 'bin/addons/stock/report/physical_inventory_counting_sheet.py'
1689--- bin/addons/stock/report/physical_inventory_counting_sheet.py 1970-01-01 00:00:00 +0000
1690+++ bin/addons/stock/report/physical_inventory_counting_sheet.py 2017-11-24 09:38:22 +0000
1691@@ -0,0 +1,61 @@
1692+# -*- coding: utf-8 -*-
1693+from report import report_sxw
1694+from spreadsheet_xml.spreadsheet_xml_write import SpreadsheetReport
1695+
1696+
1697+class CountingSheetParser(report_sxw.rml_parse):
1698+
1699+ def __init__(self, cr, uid, name, context):
1700+ super(CountingSheetParser, self).__init__(cr, uid, name, context=context)
1701+ self.counter = 0
1702+ self.localcontext.update({
1703+ 'get_headers': self.get_headers,
1704+ 'next_counter': self.get_next_counter,
1705+ 'reset_counter': self.reset_counter,
1706+ 'display_product_attributes': self.display_product_attributes,
1707+ 'yesno': self.yesno,
1708+ 'to_excel': self.to_excel,
1709+ })
1710+
1711+ @staticmethod
1712+ def to_excel(value):
1713+ if isinstance(value, report_sxw._dttime_format):
1714+ return value.format().replace(' ', 'T')
1715+ return value
1716+
1717+ @staticmethod
1718+ def display_product_attributes(item):
1719+ attributes = {'is_kc': 'KC', 'is_dg': 'DG', 'is_cs': 'CS'}
1720+ return ','.join([name for attribute, name in attributes.items() if getattr(item, attribute, False)])
1721+
1722+ @staticmethod
1723+ def yesno(value):
1724+ return 'Y' if value else 'N'
1725+
1726+ def get_next_counter(self):
1727+ self.counter += 1
1728+ return self.counter
1729+
1730+ def reset_counter(self):
1731+ self.counter = 0
1732+
1733+ def get_headers(self, objects):
1734+ # return list of cols:
1735+ # Header Name
1736+ # col type (string, date, datetime, bool, number, float, int)
1737+ # method to compute the value, Parameters: record, index, objects
1738+ return [
1739+ # ['Line Number', 'int', lambda r, index, *a: index+1],
1740+ # ['Name', 'string', lambda r, *a: r.name or ''],
1741+ # ['Stock Valuation', 'float', self.compute_stock_value],
1742+ ]
1743+
1744+SpreadsheetReport('report.physical_inventory_counting_sheet_xls', 'physical.inventory', 'addons/stock/report/physical_inventory_counting_sheet.xml', parser=CountingSheetParser)
1745+
1746+report_sxw.report_sxw(
1747+ 'report.physical_inventory_counting_sheet_pdf',
1748+ 'physical.inventory',
1749+ 'addons/stock/report/physical_inventory_counting_sheet.rml',
1750+ parser=CountingSheetParser,
1751+ header='internal'
1752+)
1753\ No newline at end of file
1754
1755=== added file 'bin/addons/stock/report/physical_inventory_counting_sheet.rml'
1756--- bin/addons/stock/report/physical_inventory_counting_sheet.rml 1970-01-01 00:00:00 +0000
1757+++ bin/addons/stock/report/physical_inventory_counting_sheet.rml 2017-11-24 09:38:22 +0000
1758@@ -0,0 +1,199 @@
1759+<?xml version="1.0"?>
1760+<document filename="counting_sheet.pdf">
1761+ <template pageSize="(29.7cm,21.0cm)" title="Stock Inventory" author="" allowSplitting="20" showBoundary="0">
1762+ <pageTemplate id="first">
1763+ <frame id="first" x1="0" y1="0" width="29.7cm" height="21.0cm"/>
1764+ </pageTemplate>
1765+ </template>
1766+ <stylesheet>
1767+ <blockTableStyle id="TableTitle">
1768+ <blockAlignment value="LEFT"/>
1769+ <blockValign value="TOP"/>
1770+ </blockTableStyle>
1771+ <blockTableStyle id="TableHeader">
1772+ <blockAlignment value="LEFT"/>
1773+ <blockValign value="TOP"/>
1774+ </blockTableStyle>
1775+ <blockTableStyle id="TableItem">
1776+ <blockAlignment value="LEFT"/>
1777+ <blockValign value="TOP"/>
1778+ <lineStyle kind="LINEBELOW" colorName="#000000" start="0,-1" stop="9,-1"/>
1779+ </blockTableStyle>
1780+ <initialize>
1781+ <paraStyle name="all" alignment="justify"/>
1782+ </initialize>
1783+ <paraStyle name="default" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="0.0" spaceAfter="0.0"/>" fontName="Helvetica" fontSize="12.0" leading="15" spaceBefore="6.0" spaceAfter="6.0"/>
1784+ <paraStyle name="title" fontName="Helvetica-Bold" fontSize="12.0" leading="15" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
1785+ <paraStyle name="table_header_label" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/> <paraStyle name="tbl_header_left" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
1786+ <paraStyle name="table_header_value" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/> <paraStyle name="tbl_header_left" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="LEFT" spaceBefore="6.0" spaceAfter="6.0"/>
1787+ <paraStyle name="table_item_header" fontName="Helvetica-Bold" fontSize="8.0" leading="10" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
1788+ <paraStyle name="table_item_data" rightIndent="0.0" leftIndent="0.0" fontName="Helvetica" fontSize="9.0" leading="11" alignment="CENTER" spaceBefore="0.0" spaceAfter="0.0"/>
1789+ </stylesheet>
1790+ <story>
1791+ <pto>
1792+ <pto_header>
1793+ <blockTable colWidths="1.5cm,2.0cm,7.5cm,2.0cm,2.0cm,2.0cm,2.0cm,2.5cm,2.0cm,2.0cm" style="TableItem">
1794+ <tr>
1795+ <td>
1796+ <para style="table_item_header">Line #</para>
1797+ </td>
1798+ <td>
1799+ <para style="table_item_header">Item code</para>
1800+ </td>
1801+ <td>
1802+ <para style="table_item_header">Description</para>
1803+ </td>
1804+ <td>
1805+ <para style="table_item_header">UoM</para>
1806+ </td>
1807+ <td>
1808+ <para style="table_item_header">Quantity counted</para>
1809+ </td>
1810+ <td>
1811+ <para style="table_item_header">Batch number</para>
1812+ </td>
1813+ <td>
1814+ <para style="table_item_header">Expiry date</para>
1815+ </td>
1816+ <td>
1817+ <para style="table_item_header">Specification</para>
1818+ </td>
1819+ <td>
1820+ <para style="table_item_header">BN managment</para>
1821+ </td>
1822+ <td>
1823+ <para style="table_item_header">ED managment</para>
1824+ </td>
1825+ </tr>
1826+ </blockTable>
1827+ </pto_header>
1828+ <para style="default">[[ repeatIn(objects,'o') ]] </para>
1829+ <blockTable colWidths="26.4cm" style="TableTitle">
1830+ <tr>
1831+ <td>
1832+ <para style="title">INVENTORY COUNTING SHEET</para>
1833+ </td>
1834+ </tr>
1835+ </blockTable>
1836+ <blockTable colWidths="7.0cm,7.0cm,7.0cm,7.0cm" style="TableHeader">
1837+ <tr>
1838+ <td>
1839+ <para style="table_header_label">Inventory counter name</para>
1840+ </td>
1841+ <td>
1842+ <para style="table_header_value">[[ o.responsible ]]</para>
1843+ </td>
1844+ <td>
1845+ <para style="table_header_label">Inventory date</para>
1846+ </td>
1847+ <td>
1848+ <para style="table_header_value">[[ formatLang(o.date,date_time=True) ]]</para>
1849+ </td>
1850+ </tr>
1851+ <tr>
1852+ <td>
1853+ <para style="table_header_label">Inventory reference</para>
1854+ </td>
1855+ <td>
1856+ <para style="table_header_value">[[ o.ref ]]</para>
1857+ </td>
1858+ <td>
1859+ <para style="table_header_label">Location</para>
1860+ </td>
1861+ <td>
1862+ <para style="table_header_value">[[ o.location_id.name ]]</para>
1863+ </td>
1864+ </tr>
1865+ <tr>
1866+ <td>
1867+ <para style="table_header_label">Inventory name</para>
1868+ </td>
1869+ <td>
1870+ <para style="table_header_value">[[ o.name ]]</para>
1871+ </td>
1872+ <td>
1873+ </td>
1874+ <td>
1875+ </td>
1876+ </tr>
1877+ </blockTable>
1878+ <para style="default">
1879+ <font color="white"> </font>
1880+ </para>
1881+ <blockTable colWidths="1.5cm,2.0cm,7.5cm,2.0cm,2.0cm,2.0cm,2.0cm,2.5cm,2.0cm,2.0cm" style="TableItem">
1882+ <tr>
1883+ <td>
1884+ <para style="table_item_header">Line #</para>
1885+ </td>
1886+ <td>
1887+ <para style="table_item_header">Item code</para>
1888+ </td>
1889+ <td>
1890+ <para style="table_item_header">Description</para>
1891+ </td>
1892+ <td>
1893+ <para style="table_item_header">UoM</para>
1894+ </td>
1895+ <td>
1896+ <para style="table_item_header">Quantity counted</para>
1897+ </td>
1898+ <td>
1899+ <para style="table_item_header">Batch number</para>
1900+ </td>
1901+ <td>
1902+ <para style="table_item_header">Expiry date</para>
1903+ </td>
1904+ <td>
1905+ <para style="table_item_header">Specification</para>
1906+ </td>
1907+ <td>
1908+ <para style="table_item_header">BN managment</para>
1909+ </td>
1910+ <td>
1911+ <para style="table_item_header">ED managment</para>
1912+ </td>
1913+ </tr>
1914+ </blockTable>
1915+ <section>
1916+ <para style="default">[[ repeatIn(o.counting_line_ids, 'p') ]]</para>
1917+ <blockTable colWidths="1.5cm,2.0cm,7.5cm,2.0cm,2.0cm,2.0cm,2.0cm,2.5cm,2.0cm,2.0cm" style="TableItem">
1918+ <tr>
1919+ <td>
1920+ <para style="table_item_data">[[ p.line_no ]]</para>
1921+ </td>
1922+ <td>
1923+ <para style="table_item_data">[[ p.product_id and p.product_id.code or '' ]]</para>
1924+ </td>
1925+ <td>
1926+ <para style="table_item_data">[[ p.product_id and p.product_id.name or '' ]]</para>
1927+ </td>
1928+ <td>
1929+ <para style="table_item_data">[[ p.product_uom_id and p.product_uom_id.name or '' ]]</para>
1930+ </td>
1931+ <td>
1932+ <para style="table_item_data">[[ p.quantity or '' ]]</para>
1933+ </td>
1934+ <td>
1935+ <para style="table_item_data">[[ p.batch_number or '' ]]</para>
1936+ </td>
1937+ <td>
1938+ <para style="table_item_data">[[ formatLang(p.expiry_date, date=True) or '' ]]</para>
1939+ </td>
1940+ <td>
1941+ <para style="table_item_data">[[ display_product_attributes(p) ]]</para>
1942+ </td>
1943+ <td>
1944+ <para style="table_item_data">[[ yesno(p.is_bn) ]]</para>
1945+ </td>
1946+ <td>
1947+ <para style="table_item_data">[[ yesno(p.is_ed) ]]</para>
1948+ </td>
1949+ </tr>
1950+ </blockTable>
1951+ </section>
1952+ <para style="default">
1953+ <font color="white"> </font>
1954+ </para>
1955+ </pto>
1956+ </story>
1957+</document>
1958\ No newline at end of file
1959
1960=== added file 'bin/addons/stock/report/physical_inventory_counting_sheet.xml'
1961--- bin/addons/stock/report/physical_inventory_counting_sheet.xml 1970-01-01 00:00:00 +0000
1962+++ bin/addons/stock/report/physical_inventory_counting_sheet.xml 2017-11-24 09:38:22 +0000
1963@@ -0,0 +1,530 @@
1964+<?xml version="1.0"?>
1965+<?mso-application progid="Excel.Sheet"?>
1966+<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
1967+ xmlns:o="urn:schemas-microsoft-com:office:office"
1968+ xmlns:x="urn:schemas-microsoft-com:office:excel"
1969+ xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
1970+ xmlns:html="http://www.w3.org/TR/REC-html40">
1971+ <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
1972+ <Author>Jean-Marc GUYON</Author>
1973+ <LastAuthor>Utilisateur Windows</LastAuthor>
1974+ <LastPrinted>2014-10-20T14:28:22Z</LastPrinted>
1975+ <Created>2014-06-02T15:56:39Z</Created>
1976+ <LastSaved>2017-09-27T08:25:31Z</LastSaved>
1977+ <Company>Microsoft</Company>
1978+ <Version>16.00</Version>
1979+ </DocumentProperties>
1980+ <OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
1981+ <AllowPNG/>
1982+ </OfficeDocumentSettings>
1983+ <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
1984+ <WindowHeight>9660</WindowHeight>
1985+ <WindowWidth>17370</WindowWidth>
1986+ <WindowTopX>480</WindowTopX>
1987+ <WindowTopY>195</WindowTopY>
1988+ <TabRatio>147</TabRatio>
1989+ <ProtectStructure>False</ProtectStructure>
1990+ <ProtectWindows>False</ProtectWindows>
1991+ </ExcelWorkbook>
1992+ <Styles>
1993+ <Style ss:ID="Default" ss:Name="Normal">
1994+ <Alignment ss:Vertical="Bottom"/>
1995+ <Borders/>
1996+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
1997+ <Interior/>
1998+ <NumberFormat/>
1999+ <Protection/>
2000+ </Style>
2001+ <Style ss:ID="m2667643071524">
2002+ <Alignment ss:Horizontal="Center" ss:Vertical="Bottom"/>
2003+ <Borders>
2004+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2005+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2006+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2007+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2008+ </Borders>
2009+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2010+ </Style>
2011+ <Style ss:ID="m2667643071544">
2012+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2013+ <Borders>
2014+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2015+ </Borders>
2016+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2017+ ss:Bold="1"/>
2018+ </Style>
2019+ <Style ss:ID="m2667643071564">
2020+ <Alignment ss:Horizontal="Center" ss:Vertical="Bottom"/>
2021+ <Borders>
2022+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2023+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2024+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2025+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2026+ </Borders>
2027+ <NumberFormat ss:Format="Short Date"/>
2028+ </Style>
2029+ <Style ss:ID="s16">
2030+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
2031+ </Style>
2032+ <Style ss:ID="s17">
2033+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2034+ </Style>
2035+ <Style ss:ID="s18">
2036+ <Borders/>
2037+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2038+ </Style>
2039+ <Style ss:ID="s19">
2040+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2041+ <Borders/>
2042+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2043+ ss:Bold="1"/>
2044+ </Style>
2045+ <Style ss:ID="s20">
2046+ <Borders>
2047+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2048+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2049+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2050+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2051+ </Borders>
2052+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2053+ </Style>
2054+ <Style ss:ID="s21">
2055+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2056+ <Borders/>
2057+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2058+ </Style>
2059+ <Style ss:ID="s22">
2060+ <Borders/>
2061+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2062+ ss:Bold="1"/>
2063+ </Style>
2064+ <Style ss:ID="s23">
2065+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2066+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"
2067+ ss:Bold="1"/>
2068+ </Style>
2069+ <Style ss:ID="s24">
2070+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2071+ <Borders>
2072+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2073+ </Borders>
2074+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2075+ ss:Bold="1"/>
2076+ </Style>
2077+ <Style ss:ID="s25">
2078+ <Borders/>
2079+ </Style>
2080+ <Style ss:ID="s34">
2081+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2082+ <Borders>
2083+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2084+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2085+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2086+ </Borders>
2087+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2088+ ss:Bold="1"/>
2089+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2090+ </Style>
2091+ <Style ss:ID="s35">
2092+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2093+ <Borders>
2094+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2095+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2096+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2097+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2098+ </Borders>
2099+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2100+ ss:Bold="1"/>
2101+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2102+ </Style>
2103+ <Style ss:ID="s36">
2104+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2105+ <Borders>
2106+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2107+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2108+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2109+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2110+ </Borders>
2111+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2112+ ss:Bold="1"/>
2113+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2114+ </Style>
2115+ <Style ss:ID="s37">
2116+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2117+ <Borders>
2118+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2119+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2120+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2121+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2122+ </Borders>
2123+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2124+ ss:Bold="1"/>
2125+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2126+ </Style>
2127+ <Style ss:ID="s38">
2128+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2129+ <Borders>
2130+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2131+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2132+ </Borders>
2133+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2134+ ss:Bold="1"/>
2135+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2136+ </Style>
2137+ <Style ss:ID="s39">
2138+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2139+ <Borders>
2140+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2141+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2142+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2143+ </Borders>
2144+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2145+ ss:Bold="1"/>
2146+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2147+ </Style>
2148+ <Style ss:ID="s40">
2149+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2150+ <Borders>
2151+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2152+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2153+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2154+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2155+ </Borders>
2156+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"
2157+ ss:Bold="1"/>
2158+ <Interior ss:Color="#D9D9D9" ss:Pattern="Solid"/>
2159+ </Style>
2160+ <Style ss:ID="s41">
2161+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2162+ <Borders>
2163+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2164+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2165+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2166+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2167+ </Borders>
2168+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"
2169+ ss:Bold="1"/>
2170+ <Interior ss:Color="#D9D9D9" ss:Pattern="Solid"/>
2171+ </Style>
2172+ <Style ss:ID="s51">
2173+ <Alignment ss:Horizontal="Center" ss:Vertical="Bottom"/>
2174+ <Borders>
2175+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2176+ </Borders>
2177+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="24" ss:Color="#000000"
2178+ ss:Bold="1"/>
2179+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2180+ </Style>
2181+ <Style ss:ID="s106">
2182+ <Alignment ss:Horizontal="Left" ss:Vertical="Center"/>
2183+ <Borders>
2184+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2185+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2186+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2187+ </Borders>
2188+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2189+ <Interior/>
2190+ </Style>
2191+ <Style ss:ID="s107">
2192+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2193+ <Borders>
2194+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2195+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2196+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2197+ </Borders>
2198+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2199+ <Interior/>
2200+ </Style>
2201+ <Style ss:ID="s108">
2202+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2203+ <Borders>
2204+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2205+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2206+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2207+ </Borders>
2208+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2209+ </Style>
2210+ <Style ss:ID="s109">
2211+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2212+ <Borders>
2213+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2214+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2215+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2216+ </Borders>
2217+ </Style>
2218+ <Style ss:ID="s209">
2219+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2220+ <Borders>
2221+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2222+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2223+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2224+ </Borders>
2225+ </Style>
2226+ <Style ss:ID="s110">
2227+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2228+ <Borders>
2229+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2230+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2231+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2232+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2233+ </Borders>
2234+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2235+ <Interior/>
2236+ </Style>
2237+ <Style ss:ID="s111">
2238+ <Alignment ss:Horizontal="Left" ss:Vertical="Center" ss:WrapText="1"/>
2239+ <Borders>
2240+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2241+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2242+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2243+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2244+ </Borders>
2245+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2246+ </Style>
2247+ <Style ss:ID="s112">
2248+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2249+ <Borders>
2250+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2251+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2252+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2253+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2254+ </Borders>
2255+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2256+ </Style>
2257+ <Style ss:ID="s113">
2258+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2259+ <Borders>
2260+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2261+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2262+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2263+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2264+ </Borders>
2265+ </Style>
2266+ <Style ss:ID="s114">
2267+ <Alignment ss:Vertical="Center"/>
2268+ <Borders>
2269+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2270+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2271+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2272+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2273+ </Borders>
2274+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2275+ </Style>
2276+ <Style ss:ID="s115">
2277+ <Alignment ss:Vertical="Center"/>
2278+ <Borders>
2279+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2280+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2281+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2282+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2283+ </Borders>
2284+ </Style>
2285+ <Style ss:ID="short_date">
2286+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2287+ <Borders>
2288+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2289+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2290+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2291+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2292+ </Borders>
2293+ <NumberFormat ss:Format="Short Date"/>
2294+ </Style>
2295+ </Styles>
2296+ <Worksheet ss:Name="stock">
2297+ <Names>
2298+ <NamedRange ss:Name="_FilterDatabase" ss:RefersTo="=stock!R9C2:R17C5"
2299+ ss:Hidden="1"/>
2300+ <NamedRange ss:Name="Print_Titles" ss:RefersTo="=stock!R1:R9"/>
2301+ <NamedRange ss:Name="Print_Area" ss:RefersTo="=stock!C2:C5"/>
2302+ </Names>
2303+ <Table x:FullColumns="1"
2304+ x:FullRows="1" ss:DefaultColumnWidth="60.75" ss:DefaultRowHeight="15">
2305+ <Column ss:AutoFitWidth="0" ss:Width="75"/>
2306+ <Column ss:AutoFitWidth="0" ss:Width="87.75"/>
2307+ <Column ss:AutoFitWidth="0" ss:Width="210.75"/>
2308+ <Column ss:AutoFitWidth="0" ss:Width="63"/>
2309+ <Column ss:AutoFitWidth="0" ss:Width="69.75"/>
2310+ <Column ss:AutoFitWidth="0" ss:Width="66"/>
2311+ <Column ss:AutoFitWidth="0" ss:Width="82.5" ss:Span="1"/>
2312+ <Column ss:Index="9" ss:AutoFitWidth="0" ss:Width="75.75"/>
2313+ <Column ss:Index="10" ss:AutoFitWidth="0" ss:Width="75.75"/>
2314+ <Row ss:Height="31.5">
2315+ <Cell ss:Index="2" ss:MergeAcross="6" ss:StyleID="s51"><Data ss:Type="String">INVENTORY COUNTING SHEET</Data><NamedCell
2316+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2317+ </Row>
2318+ <Row ss:AutoFitHeight="0" ss:Height="18.75" ss:StyleID="s16">
2319+ <Cell ss:Index="2" ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2320+ ss:Name="Print_Area"/></Cell>
2321+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2322+ ss:Name="Print_Area"/></Cell>
2323+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2324+ ss:Name="Print_Area"/></Cell>
2325+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2326+ ss:Name="Print_Area"/></Cell>
2327+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2328+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2329+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2330+ </Row>
2331+ <Row ss:AutoFitHeight="0">
2332+ <Cell ss:Index="2" ss:StyleID="s19"><Data ss:Type="String">Inventory counter Name</Data><NamedCell
2333+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2334+ <Cell ss:StyleID="s20"><Data ss:Type="String">${objects[0].responsible or ''}</Data><NamedCell
2335+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2336+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2337+ ss:Name="Print_Area"/></Cell>
2338+ <Cell ss:StyleID="s19"><Data ss:Type="String">Inventory date</Data><NamedCell
2339+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2340+ <Cell ss:MergeAcross="1" ss:StyleID="m2667643071564"><NamedCell
2341+ ss:Name="Print_Titles"/><Data ss:Type="DateTime">${ to_excel(objects[0].date) }</Data></Cell>
2342+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/></Cell>
2343+ </Row>
2344+ <Row ss:AutoFitHeight="0" ss:Height="8.25">
2345+ <Cell ss:Index="2" ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2346+ ss:Name="Print_Area"/></Cell>
2347+ <Cell ss:StyleID="s21"><NamedCell ss:Name="Print_Titles"/><NamedCell
2348+ ss:Name="Print_Area"/></Cell>
2349+ <Cell ss:StyleID="s21"><NamedCell ss:Name="Print_Titles"/><NamedCell
2350+ ss:Name="Print_Area"/></Cell>
2351+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2352+ ss:Name="Print_Area"/></Cell>
2353+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2354+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2355+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2356+ </Row>
2357+ <Row ss:Height="15.75">
2358+ <Cell ss:MergeAcross="1" ss:StyleID="m2667643071544"><Data ss:Type="String">Inventory reference</Data><NamedCell
2359+ ss:Name="Print_Titles"/></Cell>
2360+ <Cell ss:StyleID="s20"><Data ss:Type="String">${objects[0].ref}</Data><NamedCell
2361+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2362+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
2363+ ss:Name="Print_Area"/></Cell>
2364+ <Cell ss:StyleID="s22"><Data ss:Type="String">Location</Data><NamedCell
2365+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2366+ <Cell ss:MergeAcross="1" ss:StyleID="m2667643071524"><Data ss:Type="String">${objects[0].location_id and objects[0].location_id.name or ''}</Data><NamedCell
2367+ ss:Name="Print_Titles"/></Cell>
2368+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2369+ </Row>
2370+ <Row ss:AutoFitHeight="0" ss:Height="9">
2371+ <Cell ss:Index="2" ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
2372+ ss:Name="Print_Area"/></Cell>
2373+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
2374+ ss:Name="Print_Area"/></Cell>
2375+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
2376+ ss:Name="Print_Area"/></Cell>
2377+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2378+ ss:Name="Print_Area"/></Cell>
2379+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2380+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2381+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2382+ </Row>
2383+ <Row ss:Height="15.75">
2384+ <Cell ss:Index="2" ss:StyleID="s24"><Data ss:Type="String">Inventory name </Data><NamedCell
2385+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2386+ <Cell ss:StyleID="s20"><Data ss:Type="String">${objects[0].name or ''}</Data><NamedCell
2387+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
2388+ <Cell ss:StyleID="s25"><NamedCell ss:Name="Print_Titles"/><NamedCell
2389+ ss:Name="Print_Area"/></Cell>
2390+ <Cell ss:StyleID="s25"><NamedCell ss:Name="Print_Titles"/><NamedCell
2391+ ss:Name="Print_Area"/></Cell>
2392+ <Cell ss:StyleID="s25"><NamedCell ss:Name="Print_Titles"/></Cell>
2393+ <Cell ss:StyleID="s25"><NamedCell ss:Name="Print_Titles"/></Cell>
2394+ <Cell ss:StyleID="s25"><NamedCell ss:Name="Print_Titles"/></Cell>
2395+ </Row>
2396+ <Row ss:Height="15.75">
2397+ <Cell ss:Index="2" ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
2398+ ss:Name="Print_Area"/></Cell>
2399+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2400+ ss:Name="Print_Area"/></Cell>
2401+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2402+ ss:Name="Print_Area"/></Cell>
2403+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
2404+ ss:Name="Print_Area"/></Cell>
2405+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2406+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2407+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/></Cell>
2408+ </Row>
2409+ <Row ss:AutoFitHeight="0" ss:Height="31.875" ss:StyleID="s23">
2410+ <Cell ss:StyleID="s34"><Data ss:Type="String">Line #</Data><NamedCell
2411+ ss:Name="Print_Titles"/></Cell>
2412+ <Cell ss:StyleID="s35"><Data ss:Type="String">Item code</Data><NamedCell
2413+ ss:Name="Print_Titles"/><NamedCell ss:Name="_FilterDatabase"/><NamedCell
2414+ ss:Name="Print_Area"/></Cell>
2415+ <Cell ss:StyleID="s35"><Data ss:Type="String">Description</Data><NamedCell
2416+ ss:Name="Print_Titles"/><NamedCell ss:Name="_FilterDatabase"/><NamedCell
2417+ ss:Name="Print_Area"/></Cell>
2418+ <Cell ss:StyleID="s36"><Data ss:Type="String">UoM</Data><NamedCell
2419+ ss:Name="Print_Titles"/><NamedCell ss:Name="_FilterDatabase"/><NamedCell
2420+ ss:Name="Print_Area"/></Cell>
2421+ <Cell ss:StyleID="s37"><Data ss:Type="String">Quantity counted</Data><NamedCell
2422+ ss:Name="Print_Titles"/><NamedCell ss:Name="_FilterDatabase"/><NamedCell
2423+ ss:Name="Print_Area"/></Cell>
2424+ <Cell ss:StyleID="s38"><Data ss:Type="String">Batch number</Data><NamedCell
2425+ ss:Name="Print_Titles"/></Cell>
2426+ <Cell ss:StyleID="s39"><Data ss:Type="String">Expiry date</Data><NamedCell
2427+ ss:Name="Print_Titles"/></Cell>
2428+ <Cell ss:StyleID="s39"><Data ss:Type="String">Specification</Data><NamedCell
2429+ ss:Name="Print_Titles"/></Cell>
2430+ <Cell ss:StyleID="s40"><Data ss:Type="String">BN Management</Data><NamedCell
2431+ ss:Name="Print_Titles"/></Cell>
2432+ <Cell ss:StyleID="s41"><Data ss:Type="String">ED Management</Data><NamedCell
2433+ ss:Name="Print_Titles"/></Cell>
2434+ </Row>
2435+% for index,item in enumerate(objects[0].counting_line_ids):
2436+ <Row ss:AutoFitHeight="0" ss:Height="15.75">
2437+ <Cell ss:StyleID="s107"><Data ss:Type="Number">${item.line_no}</Data></Cell>
2438+ <Cell ss:StyleID="s106"><Data ss:Type="String">${item.product_id and item.product_id.code or ''}</Data><NamedCell
2439+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Area"/></Cell>
2440+ <Cell ss:StyleID="s106"><Data ss:Type="String">${item.product_id and item.product_id.name or ''}</Data><NamedCell
2441+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Area"/></Cell>
2442+ <Cell ss:StyleID="s107"><Data ss:Type="String">${item.product_uom_id and item.product_uom_id.name or ''}</Data><NamedCell
2443+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Area"/></Cell>
2444+ <Cell ss:StyleID="s108"><Data ss:Type="String">${item.quantity or ''}</Data><NamedCell
2445+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Area"/></Cell>
2446+ <Cell ss:StyleID="s108"><Data ss:Type="String">${item.batch_number or ''}</Data></Cell>
2447+ % if isDate(item.expiry_date):
2448+ <Cell ss:StyleID="short_date"><Data ss:Type="DateTime">${item.expiry_date|n}T00:00:00.000</Data></Cell>
2449+ % else:
2450+ <Cell ss:StyleID="s108"><Data ss:Type="String"></Data></Cell>
2451+ % endif
2452+ <Cell ss:StyleID="s108"><Data ss:Type="String">${display_product_attributes(item)}</Data></Cell>
2453+ <Cell ss:StyleID="s109"><Data ss:Type="String">${yesno(item.is_bn)}</Data></Cell>
2454+ <Cell ss:StyleID="s209"><Data ss:Type="String">${yesno(item.is_ed)}</Data></Cell>
2455+ </Row>
2456+% endfor
2457+ </Table>
2458+ <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
2459+ <PageSetup>
2460+ <Header x:Margin="0.31496062992125984"/>
2461+ <Footer x:Margin="0.31496062992125984" x:Data="Page &amp;P de &amp;N"/>
2462+ <PageMargins x:Bottom="0.74803149606299213" x:Left="0.23622047244094491"
2463+ x:Right="0.23622047244094491" x:Top="0.74803149606299213"/>
2464+ </PageSetup>
2465+ <FitToPage/>
2466+ <Print>
2467+ <FitHeight>0</FitHeight>
2468+ <ValidPrinterInfo/>
2469+ <PaperSizeIndex>9</PaperSizeIndex>
2470+ <Scale>99</Scale>
2471+ <HorizontalResolution>1200</HorizontalResolution>
2472+ <VerticalResolution>1200</VerticalResolution>
2473+ </Print>
2474+ <Zoom>75</Zoom>
2475+ <Selected/>
2476+ <DoNotDisplayGridlines/>
2477+ <SplitHorizontal>9</SplitHorizontal>
2478+ <TopRowBottomPane>9</TopRowBottomPane>
2479+ <ActivePane>2</ActivePane>
2480+ <Panes>
2481+ <Pane>
2482+ <Number>3</Number>
2483+ </Pane>
2484+ <Pane>
2485+ <Number>2</Number>
2486+ <ActiveRow>8</ActiveRow>
2487+ </Pane>
2488+ </Panes>
2489+ <ProtectObjects>False</ProtectObjects>
2490+ <ProtectScenarios>False</ProtectScenarios>
2491+ </WorksheetOptions>
2492+ </Worksheet>
2493+</Workbook>
2494
2495=== added file 'bin/addons/stock/report/physical_inventory_discrepancies_report.py'
2496--- bin/addons/stock/report/physical_inventory_discrepancies_report.py 1970-01-01 00:00:00 +0000
2497+++ bin/addons/stock/report/physical_inventory_discrepancies_report.py 2017-11-24 09:38:22 +0000
2498@@ -0,0 +1,42 @@
2499+# -*- coding: utf-8 -*-
2500+from report import report_sxw
2501+from spreadsheet_xml.spreadsheet_xml_write import SpreadsheetReport
2502+
2503+
2504+class DiscrepanciesReportParser(report_sxw.rml_parse):
2505+
2506+ def __init__(self, cr, uid, name, context):
2507+ super(DiscrepanciesReportParser, self).__init__(cr, uid, name, context=context)
2508+ self.counter = 0
2509+ self.localcontext.update({
2510+ 'get_headers': self.get_headers,
2511+ 'next_counter': self.get_next_counter,
2512+ 'reset_counter': self.reset_counter,
2513+ 'to_excel': self.to_excel,
2514+ })
2515+
2516+ @staticmethod
2517+ def to_excel(value):
2518+ if isinstance(value, report_sxw._dttime_format):
2519+ return value.format().replace(' ', 'T')
2520+ return value
2521+
2522+ def get_next_counter(self):
2523+ self.counter += 1
2524+ return self.counter
2525+
2526+ def reset_counter(self):
2527+ self.counter = 0
2528+
2529+ def get_headers(self, objects):
2530+ # return list of cols:
2531+ # Header Name
2532+ # col type (string, date, datetime, bool, number, float, int)
2533+ # method to compute the value, Parameters: record, index, objects
2534+ return [
2535+ # ['Line Number', 'int', lambda r, index, *a: index+1],
2536+ # ['Name', 'string', lambda r, *a: r.name or ''],
2537+ # ['Stock Valuation', 'float', self.compute_stock_value],
2538+ ]
2539+
2540+SpreadsheetReport('report.physical_inventory_discrepancies_report_xls', 'physical.inventory', 'addons/stock/report/physical_inventory_discrepancies_report.xml', parser=DiscrepanciesReportParser)
2541
2542=== added file 'bin/addons/stock/report/physical_inventory_discrepancies_report.xml'
2543--- bin/addons/stock/report/physical_inventory_discrepancies_report.xml 1970-01-01 00:00:00 +0000
2544+++ bin/addons/stock/report/physical_inventory_discrepancies_report.xml 2017-11-24 09:38:22 +0000
2545@@ -0,0 +1,882 @@
2546+<?xml version="1.0"?>
2547+<?mso-application progid="Excel.Sheet"?>
2548+<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet"
2549+ xmlns:o="urn:schemas-microsoft-com:office:office"
2550+ xmlns:x="urn:schemas-microsoft-com:office:excel"
2551+ xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"
2552+ xmlns:html="http://www.w3.org/TR/REC-html40">
2553+ <DocumentProperties xmlns="urn:schemas-microsoft-com:office:office">
2554+ <Author>Jean-Marc GUYON</Author>
2555+ <LastAuthor>Utilisateur Windows</LastAuthor>
2556+ <LastPrinted>2017-11-07T15:35:42Z</LastPrinted>
2557+ <Created>2014-06-02T15:56:39Z</Created>
2558+ <LastSaved>2017-11-07T15:59:50Z</LastSaved>
2559+ <Company>Microsoft</Company>
2560+ <Version>16.00</Version>
2561+ </DocumentProperties>
2562+ <OfficeDocumentSettings xmlns="urn:schemas-microsoft-com:office:office">
2563+ <AllowPNG/>
2564+ </OfficeDocumentSettings>
2565+ <ExcelWorkbook xmlns="urn:schemas-microsoft-com:office:excel">
2566+ <WindowHeight>9660</WindowHeight>
2567+ <WindowWidth>17370</WindowWidth>
2568+ <WindowTopX>480</WindowTopX>
2569+ <WindowTopY>195</WindowTopY>
2570+ <TabRatio>319</TabRatio>
2571+ <ProtectStructure>False</ProtectStructure>
2572+ <ProtectWindows>False</ProtectWindows>
2573+ </ExcelWorkbook>
2574+ <Styles>
2575+ <Style ss:ID="Default" ss:Name="Normal">
2576+ <Alignment ss:Vertical="Bottom"/>
2577+ <Borders/>
2578+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
2579+ <Interior/>
2580+ <NumberFormat/>
2581+ <Protection/>
2582+ </Style>
2583+ <Style ss:ID="s16" ss:Name="Pourcentage">
2584+ <NumberFormat ss:Format="0%"/>
2585+ </Style>
2586+ <Style ss:ID="s17">
2587+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
2588+ </Style>
2589+ <Style ss:ID="s18">
2590+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2591+ </Style>
2592+ <Style ss:ID="s19">
2593+ <Borders/>
2594+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2595+ </Style>
2596+ <Style ss:ID="s20">
2597+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2598+ <Borders/>
2599+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2600+ ss:Bold="1"/>
2601+ </Style>
2602+ <Style ss:ID="s21">
2603+ <Borders>
2604+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2605+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2606+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2607+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2608+ </Borders>
2609+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2610+ <Interior/>
2611+ <NumberFormat ss:Format="Short Date"/>
2612+ </Style>
2613+ <Style ss:ID="s22">
2614+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2615+ <Borders/>
2616+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2617+ </Style>
2618+ <Style ss:ID="s23">
2619+ <Borders/>
2620+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2621+ ss:Bold="1"/>
2622+ </Style>
2623+ <Style ss:ID="s24">
2624+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2625+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"
2626+ ss:Bold="1"/>
2627+ </Style>
2628+ <Style ss:ID="s25">
2629+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2630+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2631+ ss:Bold="1"/>
2632+ </Style>
2633+ <Style ss:ID="s26">
2634+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2635+ <Borders>
2636+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2637+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2638+ </Borders>
2639+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2640+ ss:Bold="1"/>
2641+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2642+ </Style>
2643+ <Style ss:ID="s27">
2644+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2645+ <Borders>
2646+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2647+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2648+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2649+ </Borders>
2650+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2651+ ss:Bold="1"/>
2652+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2653+ </Style>
2654+ <Style ss:ID="s28">
2655+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2656+ <Borders>
2657+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2658+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2659+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2660+ </Borders>
2661+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2662+ ss:Bold="1"/>
2663+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2664+ </Style>
2665+ <Style ss:ID="s29">
2666+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2667+ <Borders>
2668+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2669+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2670+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2671+ </Borders>
2672+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2673+ ss:Bold="1"/>
2674+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2675+ </Style>
2676+ <Style ss:ID="s30">
2677+ <Borders>
2678+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2679+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2680+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2681+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2682+ </Borders>
2683+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2684+ </Style>
2685+ <Style ss:ID="s31">
2686+ <Borders>
2687+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2688+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2689+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2690+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2691+ </Borders>
2692+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2693+ </Style>
2694+ <Style ss:ID="s32">
2695+ <Borders>
2696+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2697+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2698+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2699+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2700+ </Borders>
2701+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2702+ </Style>
2703+ <Style ss:ID="s33">
2704+ <Borders>
2705+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2706+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2707+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2708+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2709+ </Borders>
2710+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2711+ </Style>
2712+ <Style ss:ID="s34">
2713+ <Borders>
2714+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2715+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2716+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2717+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2718+ </Borders>
2719+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2720+ </Style>
2721+ <Style ss:ID="s35">
2722+ <Borders>
2723+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2724+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2725+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2726+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2727+ </Borders>
2728+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2729+ </Style>
2730+ <Style ss:ID="s36">
2731+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2732+ <Borders>
2733+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2734+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2735+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2736+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2737+ </Borders>
2738+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2739+ </Style>
2740+ <Style ss:ID="s37">
2741+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2742+ <Borders>
2743+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2744+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2745+ </Borders>
2746+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2747+ ss:Bold="1"/>
2748+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2749+ </Style>
2750+ <Style ss:ID="s38">
2751+ <Borders>
2752+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2753+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2754+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2755+ </Borders>
2756+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2757+ </Style>
2758+ <Style ss:ID="s39">
2759+ <Borders>
2760+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2761+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2762+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2763+ </Borders>
2764+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2765+ </Style>
2766+ <Style ss:ID="s40">
2767+ <Borders>
2768+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2769+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2770+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2771+ </Borders>
2772+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2773+ </Style>
2774+ <Style ss:ID="s41">
2775+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2776+ <Borders>
2777+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2778+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2779+ </Borders>
2780+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2781+ ss:Bold="1"/>
2782+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2783+ </Style>
2784+ <Style ss:ID="s42">
2785+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2786+ <Borders>
2787+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2788+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2789+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2790+ </Borders>
2791+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2792+ </Style>
2793+ <Style ss:ID="s43">
2794+ <Borders>
2795+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2796+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2797+ </Borders>
2798+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2799+ </Style>
2800+ <Style ss:ID="s44">
2801+ <Borders>
2802+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2803+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2804+ </Borders>
2805+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2806+ </Style>
2807+ <Style ss:ID="s45">
2808+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2809+ <Borders>
2810+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2811+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2812+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2813+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2814+ </Borders>
2815+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2816+ ss:Bold="1"/>
2817+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2818+ </Style>
2819+ <Style ss:ID="s46">
2820+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2821+ <Borders>
2822+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2823+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2824+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2825+ </Borders>
2826+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2827+ </Style>
2828+ <Style ss:ID="s47">
2829+ <Alignment ss:Horizontal="Center" ss:Vertical="Center" ss:WrapText="1"/>
2830+ <Borders>
2831+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2832+ </Borders>
2833+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2834+ ss:Bold="1"/>
2835+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2836+ </Style>
2837+ <Style ss:ID="s48">
2838+ <Borders>
2839+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2840+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2841+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2842+ </Borders>
2843+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2844+ </Style>
2845+ <Style ss:ID="s49">
2846+ <Borders>
2847+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2848+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2849+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2850+ </Borders>
2851+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2852+ </Style>
2853+ <Style ss:ID="s50">
2854+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2855+ <Borders>
2856+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2857+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2858+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2859+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2860+ </Borders>
2861+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2862+ </Style>
2863+ <Style ss:ID="s51">
2864+ <Alignment ss:Horizontal="Center" ss:Vertical="Center"/>
2865+ <Borders>
2866+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2867+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2868+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2869+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2870+ </Borders>
2871+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2872+ </Style>
2873+ <Style ss:ID="s56">
2874+ <Borders/>
2875+ </Style>
2876+ <Style ss:ID="s57">
2877+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2878+ <Borders>
2879+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2880+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2881+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2882+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2883+ </Borders>
2884+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2885+ ss:Bold="1"/>
2886+ </Style>
2887+ <Style ss:ID="s58">
2888+ <Borders>
2889+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2890+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2891+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2892+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2893+ </Borders>
2894+ </Style>
2895+ <Style ss:ID="s59">
2896+ <Borders>
2897+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2898+ </Borders>
2899+ </Style>
2900+ <Style ss:ID="s60" ss:Parent="s16">
2901+ <Borders/>
2902+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
2903+ </Style>
2904+ <Style ss:ID="s61">
2905+ <Borders>
2906+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2907+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2908+ </Borders>
2909+ </Style>
2910+ <Style ss:ID="s62">
2911+ <Borders>
2912+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2913+ </Borders>
2914+ </Style>
2915+ <Style ss:ID="s63">
2916+ <Alignment ss:Horizontal="Left" ss:Vertical="Bottom"/>
2917+ <Borders>
2918+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2919+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2920+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2921+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2922+ </Borders>
2923+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2924+ </Style>
2925+ <Style ss:ID="s64">
2926+ <Alignment ss:Horizontal="Left" ss:Vertical="Bottom"/>
2927+ <Borders>
2928+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2929+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2930+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2931+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2932+ </Borders>
2933+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
2934+ </Style>
2935+ <Style ss:ID="s65">
2936+ <Alignment ss:Horizontal="Right" ss:Vertical="Bottom"/>
2937+ <Borders>
2938+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2939+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2940+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2941+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2942+ </Borders>
2943+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
2944+ ss:Bold="1"/>
2945+ </Style>
2946+ <Style ss:ID="s66">
2947+ <Borders>
2948+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
2949+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2950+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2951+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2952+ </Borders>
2953+ </Style>
2954+ <Style ss:ID="s67" ss:Parent="s16">
2955+ <Borders>
2956+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2957+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2958+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
2959+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2960+ </Borders>
2961+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="11" ss:Color="#000000"/>
2962+ </Style>
2963+ <Style ss:ID="s68">
2964+ <Borders>
2965+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2966+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="1"/>
2967+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2968+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
2969+ </Borders>
2970+ </Style>
2971+ <Style ss:ID="s69">
2972+ <Alignment ss:Horizontal="Left" ss:Vertical="Bottom"/>
2973+ <Borders/>
2974+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="24" ss:Color="#000000"
2975+ ss:Bold="1"/>
2976+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2977+ </Style>
2978+ <Style ss:ID="s70">
2979+ <Alignment ss:Horizontal="Left" ss:Vertical="Center"/>
2980+ <Borders/>
2981+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="24" ss:Color="#000000"
2982+ ss:Bold="1"/>
2983+ <Interior ss:Color="#DCDEE2" ss:Pattern="Solid"/>
2984+ </Style>
2985+ <Style ss:ID="s71">
2986+ <Alignment ss:Horizontal="Left" ss:Vertical="Bottom"/>
2987+ </Style>
2988+ <Style ss:ID="s72">
2989+ <Borders>
2990+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
2991+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
2992+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
2993+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
2994+ </Borders>
2995+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"/>
2996+ </Style>
2997+ <Style ss:ID="s73">
2998+ <Borders>
2999+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
3000+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
3001+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="2"/>
3002+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="2"/>
3003+ </Borders>
3004+ <NumberFormat ss:Format="Short Date"/>
3005+ </Style>
3006+ <Style ss:ID="s78">
3007+ <Alignment ss:Horizontal="Center" ss:Vertical="Bottom"/>
3008+ <Borders>
3009+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="1"/>
3010+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
3011+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
3012+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
3013+ </Borders>
3014+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
3015+ ss:Bold="1"/>
3016+ </Style>
3017+ <Style ss:ID="s80">
3018+ <Alignment ss:Horizontal="Center" ss:Vertical="Bottom"/>
3019+ <Borders>
3020+ <Border ss:Position="Bottom" ss:LineStyle="Continuous" ss:Weight="2"/>
3021+ <Border ss:Position="Left" ss:LineStyle="Continuous" ss:Weight="2"/>
3022+ <Border ss:Position="Right" ss:LineStyle="Continuous" ss:Weight="1"/>
3023+ <Border ss:Position="Top" ss:LineStyle="Continuous" ss:Weight="1"/>
3024+ </Borders>
3025+ <Font ss:FontName="Calibri" x:Family="Swiss" ss:Size="9" ss:Color="#000000"
3026+ ss:Bold="1"/>
3027+ </Style>
3028+ </Styles>
3029+ <Worksheet ss:Name="stock">
3030+ <Names>
3031+ <NamedRange ss:Name="_FilterDatabase" ss:RefersTo="=stock!R10C2:R10C9"
3032+ ss:Hidden="1"/>
3033+ <NamedRange ss:Name="Print_Titles" ss:RefersTo="=stock!R1:R10"/>
3034+ <NamedRange ss:Name="Print_Area" ss:RefersTo="=stock!R1C1:R11C20"/>
3035+ </Names>
3036+ <Table x:FullColumns="1"
3037+ x:FullRows="1" ss:DefaultColumnWidth="60.75" ss:DefaultRowHeight="15">
3038+ <Column ss:Index="2" ss:AutoFitWidth="0" ss:Width="54"/>
3039+ <Column ss:AutoFitWidth="0" ss:Width="69"/>
3040+ <Column ss:AutoFitWidth="0" ss:Width="210.75"/>
3041+ <Column ss:AutoFitWidth="0" ss:Width="63" ss:Span="3"/>
3042+ <Column ss:Index="9" ss:AutoFitWidth="0" ss:Width="60" ss:Span="6"/>
3043+ <Column ss:Index="16" ss:Width="67.5"/>
3044+ <Column ss:AutoFitWidth="0" ss:Width="78" ss:Span="2"/>
3045+ <Column ss:Index="20" ss:AutoFitWidth="0" ss:Width="273"/>
3046+ <Row ss:Height="31.5" ss:StyleID="s71">
3047+ <Cell ss:StyleID="s70"><Data ss:Type="String">INVENTORY REPORT</Data><NamedCell
3048+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3049+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3050+ ss:Name="Print_Area"/></Cell>
3051+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3052+ ss:Name="Print_Area"/></Cell>
3053+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3054+ ss:Name="Print_Area"/></Cell>
3055+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3056+ ss:Name="Print_Area"/></Cell>
3057+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3058+ ss:Name="Print_Area"/></Cell>
3059+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3060+ ss:Name="Print_Area"/></Cell>
3061+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3062+ ss:Name="Print_Area"/></Cell>
3063+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3064+ ss:Name="Print_Area"/></Cell>
3065+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3066+ ss:Name="Print_Area"/></Cell>
3067+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3068+ ss:Name="Print_Area"/></Cell>
3069+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3070+ ss:Name="Print_Area"/></Cell>
3071+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3072+ ss:Name="Print_Area"/></Cell>
3073+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3074+ ss:Name="Print_Area"/></Cell>
3075+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3076+ ss:Name="Print_Area"/></Cell>
3077+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3078+ ss:Name="Print_Area"/></Cell>
3079+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3080+ ss:Name="Print_Area"/></Cell>
3081+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3082+ ss:Name="Print_Area"/></Cell>
3083+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3084+ ss:Name="Print_Area"/></Cell>
3085+ <Cell ss:StyleID="s69"><NamedCell ss:Name="Print_Titles"/><NamedCell
3086+ ss:Name="Print_Area"/></Cell>
3087+ </Row>
3088+ <Row ss:AutoFitHeight="0" ss:Height="18.75" ss:StyleID="s17">
3089+ <Cell ss:Index="2" ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3090+ ss:Name="Print_Area"/></Cell>
3091+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3092+ ss:Name="Print_Area"/></Cell>
3093+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3094+ ss:Name="Print_Area"/></Cell>
3095+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3096+ ss:Name="Print_Area"/></Cell>
3097+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3098+ ss:Name="Print_Area"/></Cell>
3099+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3100+ ss:Name="Print_Area"/></Cell>
3101+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3102+ ss:Name="Print_Area"/></Cell>
3103+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3104+ ss:Name="Print_Area"/></Cell>
3105+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3106+ ss:Name="Print_Area"/></Cell>
3107+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3108+ ss:Name="Print_Area"/></Cell>
3109+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3110+ ss:Name="Print_Area"/></Cell>
3111+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3112+ ss:Name="Print_Area"/></Cell>
3113+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3114+ ss:Name="Print_Area"/></Cell>
3115+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3116+ ss:Name="Print_Area"/></Cell>
3117+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3118+ ss:Name="Print_Area"/></Cell>
3119+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3120+ ss:Name="Print_Area"/></Cell>
3121+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3122+ ss:Name="Print_Area"/></Cell>
3123+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3124+ ss:Name="Print_Area"/></Cell>
3125+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3126+ ss:Name="Print_Area"/></Cell>
3127+ </Row>
3128+ <Row ss:AutoFitHeight="0">
3129+ <Cell ss:Index="2" ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3130+ ss:Name="Print_Area"/></Cell>
3131+ <Cell ss:StyleID="s20"><Data ss:Type="String">Inventory Name </Data><NamedCell
3132+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3133+ <Cell ss:StyleID="s72"><Data ss:Type="String">${ objects[0].name or '' }</Data><NamedCell ss:Name="Print_Titles"/><NamedCell
3134+ ss:Name="Print_Area"/></Cell>
3135+ <Cell ss:Index="8" ss:StyleID="s20"><Data ss:Type="String">Inventory date</Data><NamedCell
3136+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3137+ <Cell ss:StyleID="s73"><NamedCell ss:Name="Print_Titles"/><Data ss:Type="DateTime">${ to_excel(objects[0].date) }</Data><NamedCell
3138+ ss:Name="Print_Area"/></Cell>
3139+ <Cell ss:Index="17" ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3140+ ss:Name="Print_Area"/></Cell>
3141+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3142+ ss:Name="Print_Area"/></Cell>
3143+ <Cell ss:StyleID="s20"><Data ss:Type="String">Location</Data><NamedCell
3144+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3145+ <Cell ss:StyleID="s21"><Data ss:Type="String">${ objects[0].location_id and objects[0].location_id.name or '' }</Data><NamedCell ss:Name="Print_Titles"/><NamedCell
3146+ ss:Name="Print_Area"/></Cell>
3147+ </Row>
3148+ <Row ss:AutoFitHeight="0" ss:Height="8.25">
3149+ <Cell ss:Index="2" ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3150+ ss:Name="Print_Area"/></Cell>
3151+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3152+ ss:Name="Print_Area"/></Cell>
3153+ <Cell ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
3154+ ss:Name="Print_Area"/></Cell>
3155+ <Cell ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
3156+ ss:Name="Print_Area"/></Cell>
3157+ <Cell ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
3158+ ss:Name="Print_Area"/></Cell>
3159+ <Cell ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
3160+ ss:Name="Print_Area"/></Cell>
3161+ <Cell ss:StyleID="s22"><NamedCell ss:Name="Print_Titles"/><NamedCell
3162+ ss:Name="Print_Area"/></Cell>
3163+ </Row>
3164+ <Row>
3165+ <Cell ss:Index="5" ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3166+ ss:Name="Print_Area"/></Cell>
3167+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3168+ ss:Name="Print_Area"/></Cell>
3169+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3170+ ss:Name="Print_Area"/></Cell>
3171+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3172+ ss:Name="Print_Area"/></Cell>
3173+ <Cell ss:Index="16" ss:StyleID="s61"><NamedCell ss:Name="Print_Titles"/><NamedCell
3174+ ss:Name="Print_Area"/></Cell>
3175+ <Cell ss:StyleID="s62"><NamedCell ss:Name="Print_Titles"/><NamedCell
3176+ ss:Name="Print_Area"/></Cell>
3177+ <Cell ss:StyleID="s63"><Data ss:Type="String">Number of line</Data><NamedCell
3178+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3179+ <Cell ss:StyleID="s63"><Data ss:Type="String">Value (CHF)</Data><NamedCell
3180+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3181+ <Cell ss:StyleID="s64"><Data ss:Type="String">Total Absolute value</Data><NamedCell
3182+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3183+ </Row>
3184+ <Row ss:Height="15.75">
3185+ <Cell ss:Index="2" ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3186+ ss:Name="Print_Area"/></Cell>
3187+ <Cell ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3188+ ss:Name="Print_Area"/></Cell>
3189+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3190+ ss:Name="Print_Area"/></Cell>
3191+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3192+ ss:Name="Print_Area"/></Cell>
3193+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3194+ ss:Name="Print_Area"/></Cell>
3195+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3196+ ss:Name="Print_Area"/></Cell>
3197+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3198+ ss:Name="Print_Area"/></Cell>
3199+ <Cell ss:Index="16" ss:MergeAcross="1" ss:StyleID="s78"><Data ss:Type="String">Total at the end</Data><NamedCell
3200+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3201+ <Cell ss:StyleID="s58"><NamedCell ss:Name="Print_Titles"/><Data ss:Type="String">${objects[0].inventory_lines_number}</Data><NamedCell
3202+ ss:Name="Print_Area"/></Cell>
3203+ <Cell ss:StyleID="s57"><NamedCell ss:Name="Print_Titles"/><Data ss:Type="String">${objects[0].inventory_lines_value}</Data><NamedCell
3204+ ss:Name="Print_Area"/></Cell>
3205+ <Cell ss:StyleID="s65"><NamedCell ss:Name="Print_Titles"/><Data ss:Type="String">${objects[0].inventory_lines_absvalue}</Data><NamedCell
3206+ ss:Name="Print_Area"/></Cell>
3207+ </Row>
3208+ <Row ss:Height="15.75">
3209+ <Cell ss:Index="3" ss:StyleID="s20"><Data ss:Type="String">Inventory resp Name</Data><NamedCell
3210+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3211+ <Cell ss:StyleID="s72"><Data ss:Type="String">${ objects[0].responsible or '' }</Data><NamedCell ss:Name="Print_Titles"/><NamedCell
3212+ ss:Name="Print_Area"/></Cell>
3213+ <Cell ss:Index="8" ss:StyleID="s25"><Data ss:Type="String">Signature of responsible </Data><NamedCell
3214+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3215+ <Cell ss:StyleID="s59"><NamedCell ss:Name="Print_Titles"/><NamedCell
3216+ ss:Name="Print_Area"/></Cell>
3217+ <Cell ss:StyleID="s59"><NamedCell ss:Name="Print_Titles"/><NamedCell
3218+ ss:Name="Print_Area"/></Cell>
3219+ <Cell ss:StyleID="s59"><NamedCell ss:Name="Print_Titles"/><NamedCell
3220+ ss:Name="Print_Area"/></Cell>
3221+ <Cell ss:StyleID="s59"><NamedCell ss:Name="Print_Titles"/><NamedCell
3222+ ss:Name="Print_Area"/></Cell>
3223+ <Cell ss:StyleID="s59"><NamedCell ss:Name="Print_Titles"/><NamedCell
3224+ ss:Name="Print_Area"/></Cell>
3225+ <Cell ss:StyleID="s56"><NamedCell ss:Name="Print_Titles"/><NamedCell
3226+ ss:Name="Print_Area"/></Cell>
3227+ <Cell ss:StyleID="s18"><NamedCell ss:Name="Print_Titles"/><NamedCell
3228+ ss:Name="Print_Area"/></Cell>
3229+ <Cell ss:MergeAcross="1" ss:StyleID="s78"><Data ss:Type="String">With discrepencies</Data><NamedCell
3230+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3231+ <Cell ss:StyleID="s58"><Data ss:Type="String">${objects[0].discrepancy_lines_number}</Data><NamedCell ss:Name="Print_Titles"/><NamedCell
3232+ ss:Name="Print_Area"/></Cell>
3233+ <Cell ss:StyleID="s58"><Data ss:Type="String">${objects[0].discrepancy_lines_value}</Data><NamedCell ss:Name="Print_Titles"/><NamedCell
3234+ ss:Name="Print_Area"/></Cell>
3235+ <Cell ss:StyleID="s66"><Data ss:Type="String">${objects[0].discrepancy_lines_absvalue}</Data><NamedCell ss:Name="Print_Titles"/><NamedCell
3236+ ss:Name="Print_Area"/></Cell>
3237+ </Row>
3238+ <Row ss:Height="15.75">
3239+ <Cell ss:Index="2" ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3240+ ss:Name="Print_Area"/></Cell>
3241+ <Cell ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3242+ ss:Name="Print_Area"/></Cell>
3243+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3244+ ss:Name="Print_Area"/></Cell>
3245+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3246+ ss:Name="Print_Area"/></Cell>
3247+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3248+ ss:Name="Print_Area"/></Cell>
3249+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3250+ ss:Name="Print_Area"/></Cell>
3251+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3252+ ss:Name="Print_Area"/></Cell>
3253+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3254+ ss:Name="Print_Area"/></Cell>
3255+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3256+ ss:Name="Print_Area"/></Cell>
3257+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3258+ ss:Name="Print_Area"/></Cell>
3259+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3260+ ss:Name="Print_Area"/></Cell>
3261+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3262+ ss:Name="Print_Area"/></Cell>
3263+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3264+ ss:Name="Print_Area"/></Cell>
3265+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3266+ ss:Name="Print_Area"/></Cell>
3267+ <Cell ss:MergeAcross="1" ss:StyleID="s80"><Data ss:Type="String">%</Data><NamedCell
3268+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3269+ <Cell ss:StyleID="s67"><Data ss:Type="String">${objects[0].discrepancy_lines_percent}</Data><NamedCell
3270+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3271+ <Cell ss:StyleID="s67"><Data ss:Type="String">${objects[0].discrepancy_lines_percent_value}</Data><NamedCell
3272+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3273+ <Cell ss:StyleID="s68"><Data ss:Type="String">${objects[0].discrepancy_lines_percent_absvalue}</Data><NamedCell
3274+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3275+ </Row>
3276+ <Row ss:Height="15.75">
3277+ <Cell ss:Index="2" ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3278+ ss:Name="Print_Area"/></Cell>
3279+ <Cell ss:StyleID="s23"><NamedCell ss:Name="Print_Titles"/><NamedCell
3280+ ss:Name="Print_Area"/></Cell>
3281+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3282+ ss:Name="Print_Area"/></Cell>
3283+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3284+ ss:Name="Print_Area"/></Cell>
3285+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3286+ ss:Name="Print_Area"/></Cell>
3287+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3288+ ss:Name="Print_Area"/></Cell>
3289+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3290+ ss:Name="Print_Area"/></Cell>
3291+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3292+ ss:Name="Print_Area"/></Cell>
3293+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3294+ ss:Name="Print_Area"/></Cell>
3295+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3296+ ss:Name="Print_Area"/></Cell>
3297+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3298+ ss:Name="Print_Area"/></Cell>
3299+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3300+ ss:Name="Print_Area"/></Cell>
3301+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3302+ ss:Name="Print_Area"/></Cell>
3303+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3304+ ss:Name="Print_Area"/></Cell>
3305+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3306+ ss:Name="Print_Area"/></Cell>
3307+ <Cell ss:StyleID="s19"><NamedCell ss:Name="Print_Titles"/><NamedCell
3308+ ss:Name="Print_Area"/></Cell>
3309+ <Cell ss:StyleID="s20"><NamedCell ss:Name="Print_Titles"/><NamedCell
3310+ ss:Name="Print_Area"/></Cell>
3311+ <Cell ss:StyleID="s60"><NamedCell ss:Name="Print_Titles"/><NamedCell
3312+ ss:Name="Print_Area"/></Cell>
3313+ <Cell ss:StyleID="s60"><NamedCell ss:Name="Print_Titles"/><NamedCell
3314+ ss:Name="Print_Area"/></Cell>
3315+ <Cell ss:StyleID="s56"><NamedCell ss:Name="Print_Titles"/></Cell>
3316+ </Row>
3317+ <Row ss:AutoFitHeight="0" ss:Height="46.5" ss:StyleID="s24">
3318+ <Cell ss:StyleID="s26"><Data ss:Type="String">Line #</Data><NamedCell
3319+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3320+ <Cell ss:StyleID="s26"><Data ss:Type="String">Family</Data><NamedCell
3321+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3322+ ss:Name="Print_Area"/></Cell>
3323+ <Cell ss:StyleID="s27"><Data ss:Type="String">Item code</Data><NamedCell
3324+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3325+ ss:Name="Print_Area"/></Cell>
3326+ <Cell ss:StyleID="s27"><Data ss:Type="String">Description</Data><NamedCell
3327+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3328+ ss:Name="Print_Area"/></Cell>
3329+ <Cell ss:StyleID="s37"><Data ss:Type="String">UoM</Data><NamedCell
3330+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3331+ ss:Name="Print_Area"/></Cell>
3332+ <Cell ss:StyleID="s45"><Data ss:Type="String">Unit price</Data><NamedCell
3333+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3334+ ss:Name="Print_Area"/></Cell>
3335+ <Cell ss:StyleID="s26"><Data ss:Type="String">currency (functional)</Data><NamedCell
3336+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3337+ ss:Name="Print_Area"/></Cell>
3338+ <Cell ss:StyleID="s26"><Data ss:Type="String">Quantity Theorical</Data><NamedCell
3339+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3340+ ss:Name="Print_Area"/></Cell>
3341+ <Cell ss:StyleID="s29"><Data ss:Type="String">Quantity counted</Data><NamedCell
3342+ ss:Name="_FilterDatabase"/><NamedCell ss:Name="Print_Titles"/><NamedCell
3343+ ss:Name="Print_Area"/></Cell>
3344+ <Cell ss:StyleID="s47"><Data ss:Type="String">Batch no</Data><NamedCell
3345+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3346+ <Cell ss:StyleID="s28"><Data ss:Type="String">Expiry date</Data><NamedCell
3347+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3348+ <Cell ss:StyleID="s41"><Data ss:Type="String">Discrepancy</Data><NamedCell
3349+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3350+ <Cell ss:StyleID="s41"><Data ss:Type="String">Discrepancy value</Data><NamedCell
3351+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3352+ <Cell ss:StyleID="s41"><Data ss:Type="String">Total QTY before INV</Data><NamedCell
3353+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3354+ <Cell ss:StyleID="s28"><Data ss:Type="String">Total QTY after INV</Data><NamedCell
3355+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3356+ <Cell ss:StyleID="s28"><Data ss:Type="String">Total Value after INV</Data><NamedCell
3357+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3358+ <Cell ss:StyleID="s28"><Data ss:Type="String">Discrepancy</Data><NamedCell
3359+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3360+ <Cell ss:StyleID="s45"><Data ss:Type="String">Discrepancy Value</Data><NamedCell
3361+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3362+ <Cell ss:StyleID="s45"><Data ss:Type="String">Adjustment type</Data><NamedCell
3363+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3364+ <Cell ss:StyleID="s45"><Data ss:Type="String">Comments / actions (in case of discrepancy)</Data><NamedCell
3365+ ss:Name="Print_Titles"/><NamedCell ss:Name="Print_Area"/></Cell>
3366+ </Row>
3367+ % for index,item in enumerate(objects[0].discrepancy_line_ids):
3368+ <Row>
3369+ <Cell ss:StyleID="s30"><Data ss:Type="String">${item.line_no}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3370+ <Cell ss:StyleID="s30"><Data ss:Type="String">${item.nomen_manda_2 and item.nomen_manda_2.name or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3371+ <Cell ss:StyleID="s31"><Data ss:Type="String">${item.product_id and item.product_id.code or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3372+ <Cell ss:StyleID="s31"><Data ss:Type="String">${item.product_id and item.product_id.name or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3373+ <Cell ss:StyleID="s38"><Data ss:Type="String">${item.product_uom_id and item.product_uom_id.name or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3374+ <Cell ss:StyleID="s43"><Data ss:Type="String">${item.standard_price}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3375+ <Cell ss:StyleID="s43"><Data ss:Type="String">${item.currency_id and item.currency_id.name or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3376+ <Cell ss:StyleID="s30"><Data ss:Type="String">${item.theoretical_qty}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3377+ <Cell ss:StyleID="s31"><Data ss:Type="String">${item.counted_qty}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3378+ <Cell ss:StyleID="s38"><Data ss:Type="String">${item.batch_number or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3379+ <Cell ss:StyleID="s32"><Data ss:Type="String">${item.expiry_date or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3380+ <Cell ss:StyleID="s43"><Data ss:Type="String">${item.discrepancy_qty}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3381+ <Cell ss:StyleID="s43"><Data ss:Type="String">${item.discrepancy_value}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3382+ <Cell ss:StyleID="s42"><Data ss:Type="String">${item.total_product_theoretical_qty}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3383+ <Cell ss:StyleID="s36"><Data ss:Type="String">${item.total_product_counted_qty}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3384+ <Cell ss:StyleID="s36"><Data ss:Type="String">${item.total_product_counted_value}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3385+ <Cell ss:StyleID="s36"><Data ss:Type="String">${item.total_product_discrepancy_qty}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3386+ <Cell ss:StyleID="s46"><Data ss:Type="String">${item.total_product_discrepancy_value}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3387+ <Cell ss:StyleID="s50"><Data ss:Type="String">${item.reason_type_id and item.reason_type_id.name or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3388+ <Cell ss:StyleID="s48"><Data ss:Type="String">${item.comment or ''}</Data><NamedCell ss:Name="Print_Area"/></Cell>
3389+ </Row>
3390+ % endfor
3391+ </Table>
3392+ <WorksheetOptions xmlns="urn:schemas-microsoft-com:office:excel">
3393+ <PageSetup>
3394+ <Layout x:Orientation="Landscape"/>
3395+ <Header x:Margin="0.31496062992125984"/>
3396+ <Footer x:Margin="0.31496062992125984" x:Data="Page &amp;P de &amp;N"/>
3397+ <PageMargins x:Bottom="0.74803149606299213" x:Left="0.23622047244094491"
3398+ x:Right="0.23622047244094491" x:Top="0.74803149606299213"/>
3399+ </PageSetup>
3400+ <FitToPage/>
3401+ <Print>
3402+ <FitHeight>0</FitHeight>
3403+ <ValidPrinterInfo/>
3404+ <PaperSizeIndex>9</PaperSizeIndex>
3405+ <Scale>46</Scale>
3406+ <HorizontalResolution>1200</HorizontalResolution>
3407+ <VerticalResolution>1200</VerticalResolution>
3408+ </Print>
3409+ <Selected/>
3410+ <DoNotDisplayGridlines/>
3411+ <SplitHorizontal>10</SplitHorizontal>
3412+ <TopRowBottomPane>10</TopRowBottomPane>
3413+ <ActivePane>2</ActivePane>
3414+ <Panes>
3415+ <Pane>
3416+ <Number>3</Number>
3417+ </Pane>
3418+ <Pane>
3419+ <Number>2</Number>
3420+ <ActiveRow>0</ActiveRow>
3421+ </Pane>
3422+ </Panes>
3423+ <ProtectObjects>False</ProtectObjects>
3424+ <ProtectScenarios>False</ProtectScenarios>
3425+ </WorksheetOptions>
3426+ </Worksheet>
3427+</Workbook>
3428
3429=== added file 'bin/addons/stock/report/physical_inventory_view.xml'
3430--- bin/addons/stock/report/physical_inventory_view.xml 1970-01-01 00:00:00 +0000
3431+++ bin/addons/stock/report/physical_inventory_view.xml 2017-11-24 09:38:22 +0000
3432@@ -0,0 +1,30 @@
3433+<?xml version="1.0" encoding="utf-8" ?>
3434+<openerp>
3435+ <data>
3436+ <report
3437+ id="physical_inventory_counting_sheet_xls"
3438+ string="Export xls counting sheet"
3439+ model="physical.inventory"
3440+ name="physical_inventory_counting_sheet_xls"
3441+ report_type="webkit"
3442+ menu="False"
3443+ />
3444+ <report
3445+ id="physical_inventory_counting_sheet_pdf"
3446+ string="Export pdf counting sheet"
3447+ model="physical.inventory"
3448+ name="physical_inventory_counting_sheet_pdf"
3449+ rml="addons/stock/report/physical_inventory_counting_sheet.rml"
3450+ header="False"
3451+ menu="False"
3452+ />
3453+ <report
3454+ id="physical_inventory_report_xls"
3455+ string="Export xls inventory report"
3456+ model="physical.inventory"
3457+ name="physical_inventory_discrepancies_report_xls"
3458+ report_type="webkit"
3459+ menu="False"
3460+ />
3461+ </data>
3462+</openerp>
3463\ No newline at end of file
3464
3465=== modified file 'bin/addons/stock/stock_view.xml'
3466--- bin/addons/stock/stock_view.xml 2017-11-03 10:08:12 +0000
3467+++ bin/addons/stock/stock_view.xml 2017-11-24 09:38:22 +0000
3468@@ -190,7 +190,6 @@
3469 <field name="search_view_id" ref="view_inventory_filter" />
3470 <field name="help">Periodical Inventories are used to count the number of products available per location. You can use it once a year when you do the general inventory or whenever you need it, to correct the current stock level of a product.</field>
3471 </record>
3472- <menuitem action="action_inventory_form" id="menu_action_inventory_form" parent="menu_stock_inventory_control" sequence="30"/>
3473
3474 <record id="action_inventory_form_draft" model="ir.actions.act_window">
3475 <field name="name">Draft Physical Inventories</field>
3476
3477=== modified file 'bin/addons/stock/wizard/__init__.py'
3478--- bin/addons/stock/wizard/__init__.py 2011-01-14 00:11:01 +0000
3479+++ bin/addons/stock/wizard/__init__.py 2017-11-24 09:38:22 +0000
3480@@ -34,5 +34,8 @@
3481 import stock_change_standard_price
3482 import stock_return_picking
3483 import stock_change_product_qty
3484+import physical_inventory_select_products
3485+import physical_inventory_generate_counting_sheet
3486+import physical_inventory_import
3487 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
3488
3489
3490=== added file 'bin/addons/stock/wizard/physical_inventory_generate_counting_sheet.py'
3491--- bin/addons/stock/wizard/physical_inventory_generate_counting_sheet.py 1970-01-01 00:00:00 +0000
3492+++ bin/addons/stock/wizard/physical_inventory_generate_counting_sheet.py 2017-11-24 09:38:22 +0000
3493@@ -0,0 +1,198 @@
3494+# -*- coding: utf-8 -*-
3495+##############################################################################
3496+#
3497+# OpenERP, Open Source Management Solution
3498+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
3499+#
3500+# This program is free software: you can redistribute it and/or modify
3501+# it under the terms of the GNU Affero General Public License as
3502+# published by the Free Software Foundation, either version 3 of the
3503+# License, or (at your option) any later version.
3504+#
3505+# This program is distributed in the hope that it will be useful,
3506+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3507+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3508+# GNU Affero General Public License for more details.
3509+#
3510+# You should have received a copy of the GNU Affero General Public License
3511+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3512+#
3513+##############################################################################
3514+
3515+from osv import fields, osv
3516+from tools.translate import _
3517+
3518+class physical_inventory_generate_counting_sheet(osv.osv_memory):
3519+ _name = "physical.inventory.generate.counting.sheet"
3520+ _description = "Generate counting sheet from selected products"
3521+
3522+ _columns = {
3523+ 'inventory_id': fields.many2one('physical.inventory', 'Inventory', readonly=True),
3524+ 'prefill_bn': fields.boolean('Prefill Batch Numbers'),
3525+ 'prefill_ed': fields.boolean('Prefill Expiry Dates'),
3526+ }
3527+
3528+ _defaults = {
3529+ 'prefill_bn': True,
3530+ 'prefill_ed': True
3531+ }
3532+
3533+ def create(self, cr, user, vals, context=None):
3534+
3535+ context = context if context else {}
3536+
3537+ assert 'inventory_id' in vals
3538+
3539+ return super(physical_inventory_generate_counting_sheet, self).create(cr, user, vals, context=context)
3540+
3541+
3542+ def generate_counting_sheet(self, cr, uid, wizard_ids, context=None):
3543+ context = context if context else {}
3544+ def read_single(model, id_, column):
3545+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
3546+ def read_many(model, ids, columns):
3547+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
3548+ def write(model, id_, vals):
3549+ return self.pool.get(model).write(cr, uid, [id_], vals, context=context)
3550+
3551+ # Get this wizard...
3552+ assert len(wizard_ids) == 1
3553+ wizard_id = wizard_ids[0]
3554+
3555+ # Get the selected option
3556+ prefill_bn = read_single(self._name, wizard_id, 'prefill_bn')
3557+ prefill_ed = read_single(self._name, wizard_id, 'prefill_ed')
3558+
3559+ # Get location, products selected, and existing inventory lines
3560+ inventory_id = read_single(self._name, wizard_id, "inventory_id")
3561+ inventory = read_many("physical.inventory", [inventory_id], ['location_id',
3562+ 'counting_line_ids',
3563+ 'product_ids'])[0]
3564+
3565+ # Get relevant info for products to be able to create the inventory
3566+ # lines
3567+ location_id = inventory["location_id"][0]
3568+ product_ids = inventory["product_ids"]
3569+
3570+ bn_and_eds = self.get_BN_and_ED_for_products_at_location(cr, uid, location_id, product_ids, context=context)
3571+
3572+ # Prepare the inventory lines to be created
3573+
3574+ inventory_counting_lines_to_create = []
3575+ for product_id in product_ids:
3576+ bn_and_eds_for_this_product = bn_and_eds[product_id]
3577+ # If no bn / ed related to this product, create a single inventory
3578+ # line
3579+ if len(bn_and_eds_for_this_product) == 0:
3580+ values = { "line_no": len(inventory_counting_lines_to_create) + 1,
3581+ "inventory_id": inventory_id,
3582+ "product_id": product_id,
3583+ "batch_number": False,
3584+ "expiry_date": False
3585+ }
3586+ inventory_counting_lines_to_create.append(values)
3587+ # Otherwise, create an inventory line for this product ~and~ for
3588+ # each BN/ED
3589+ else:
3590+ for bn_and_ed in bn_and_eds_for_this_product:
3591+ values = { "line_no": len(inventory_counting_lines_to_create) + 1,
3592+ "inventory_id": inventory_id,
3593+ "product_id": product_id,
3594+ "batch_number": bn_and_ed[0] if prefill_bn else False,
3595+ "expiry_date": bn_and_ed[1] if prefill_ed else False
3596+ }
3597+ inventory_counting_lines_to_create.append(values)
3598+
3599+ # Get the existing inventory counting lines (to be cleared)
3600+
3601+ existing_inventory_counting_lines = inventory["counting_line_ids"]
3602+
3603+ # Prepare the actual create/remove for inventory lines
3604+ # 2 is the code for removal/deletion, 0 is for addition/creation
3605+
3606+ delete_existing_inventory_counting_lines = [ (2,line_id) for line_id in existing_inventory_counting_lines ]
3607+
3608+ create_inventory_counting_lines = [ (0,0,line_values) for line_values in inventory_counting_lines_to_create ]
3609+
3610+ todo = []
3611+ todo.extend(delete_existing_inventory_counting_lines)
3612+ todo.extend(create_inventory_counting_lines)
3613+
3614+ # Do the actual write
3615+ # TODO : Test if Draft state here
3616+ write("physical.inventory", inventory_id, {'counting_line_ids': todo,
3617+ 'state': 'counting'})
3618+
3619+ return {'type': 'ir.actions.act_window_close'}
3620+
3621+
3622+ def get_BN_and_ED_for_products_at_location(self, cr, uid, location_id, product_ids, context=None):
3623+ context = context if context else {}
3624+ def read_many(model, ids, columns):
3625+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
3626+
3627+ # Get the moves at location, related to these products
3628+ moves_at_location = self.get_moves_at_location(cr, uid, location_id, context=context)
3629+
3630+ moves_at_location_for_products = [ m for m in moves_at_location
3631+ if m["product_id"][0] in product_ids ]
3632+
3633+ # Get the production lot associated to these moves
3634+ moves_at_location_for_products = read_many("stock.move",
3635+ moves_at_location_for_products,
3636+ ["product_id",
3637+ "prodlot_id",
3638+ "expired_date"])
3639+
3640+ # Init a dict with an empty set for each products
3641+ BN_and_ED = { product_id:set() for product_id in product_ids }
3642+
3643+ for move in moves_at_location_for_products:
3644+
3645+ product_id = move["product_id"][0]
3646+
3647+ batch_number = move["prodlot_id"][1] if isinstance(move["prodlot_id"], tuple) else False
3648+ expired_date = move["expired_date"]
3649+
3650+ # Dirty hack to ignore/hide internal batch numbers ("MSFBN")
3651+ if batch_number and batch_number.startswith("MSFBN"):
3652+ batch_number = False
3653+
3654+ BN_and_ED[product_id].add((batch_number, expired_date))
3655+
3656+ return BN_and_ED
3657+
3658+
3659+ # FIXME : this is copy/pasta from the other wizard ...
3660+ # Should be factorized, probably in physical inventory, or stock somewhere.
3661+ def get_moves_at_location(self, cr, uid, location_id, context=None):
3662+ context = context if context else {}
3663+
3664+ def read_many(model, ids, columns):
3665+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
3666+
3667+ def search(model, domain):
3668+ return self.pool.get(model).search(cr, uid, domain, context=context)
3669+
3670+ assert isinstance(location_id, int)
3671+
3672+ # Get all the moves for in/out of that location
3673+ from_or_to_location = ['&', '|',
3674+ ('location_id', 'in', [location_id]),
3675+ ('location_dest_id', 'in', [location_id]),
3676+ ('state', '=', 'done')]
3677+
3678+ moves_at_location_ids = search("stock.move", from_or_to_location)
3679+ moves_at_location = read_many("stock.move",
3680+ moves_at_location_ids,
3681+ ["product_id",
3682+ "date",
3683+ "product_qty",
3684+ "location_id",
3685+ "location_dest_id"])
3686+
3687+ return moves_at_location
3688+
3689+physical_inventory_generate_counting_sheet()
3690+
3691+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
3692
3693=== added file 'bin/addons/stock/wizard/physical_inventory_generate_counting_sheet_view.xml'
3694--- bin/addons/stock/wizard/physical_inventory_generate_counting_sheet_view.xml 1970-01-01 00:00:00 +0000
3695+++ bin/addons/stock/wizard/physical_inventory_generate_counting_sheet_view.xml 2017-11-24 09:38:22 +0000
3696@@ -0,0 +1,17 @@
3697+<?xml version="1.0" encoding="utf-8"?>
3698+<openerp>
3699+ <data>
3700+ <record id="physical_inventory_generate_counting_sheet" model="ir.ui.view">
3701+ <field name="name">Generate Counting Sheet</field>
3702+ <field name="model">physical.inventory.generate.counting.sheet</field>
3703+ <field name="type">form</field>
3704+ <field name="arch" type="xml">
3705+ <form string="Generate Counting Sheet">
3706+ <field name="prefill_bn" colspan="4"/>
3707+ <field name="prefill_ed" colspan="4"/>
3708+ <button name="generate_counting_sheet" string="Generate counting sheet" icon="gtk-go-forward" colspan="4" type="object" />
3709+ </form>
3710+ </field>
3711+ </record>
3712+ </data>
3713+</openerp>
3714
3715=== added file 'bin/addons/stock/wizard/physical_inventory_import.py'
3716--- bin/addons/stock/wizard/physical_inventory_import.py 1970-01-01 00:00:00 +0000
3717+++ bin/addons/stock/wizard/physical_inventory_import.py 2017-11-24 09:38:22 +0000
3718@@ -0,0 +1,105 @@
3719+# -*- coding: utf-8 -*-
3720+
3721+from osv import fields, osv
3722+
3723+
3724+class PhysicalInventoryImportWizard(osv.osv_memory):
3725+ _name = "physical.inventory.import.wizard"
3726+ _description = "Physical inventory import wizard"
3727+
3728+ _columns = {
3729+ 'message': fields.text('Message', readonly=True),
3730+ 'line_ids': fields.one2many('physical.inventory.import.line.wizard', 'parent_id', string='Details')
3731+ }
3732+
3733+ def action_close(self, cr, uid, ids, context=None):
3734+ return self.close_action()
3735+
3736+ def message_box(self, cr, uid, title, message, context=None):
3737+ return {
3738+ 'name': title,
3739+ 'view_type': 'form',
3740+ 'view_mode': 'form',
3741+ 'res_model': self._name,
3742+ 'res_id': self.create(cr, uid, {'message': message}, context=context),
3743+ 'type': 'ir.actions.act_window',
3744+ 'view_id': [self.get_view_by_name(cr, uid, 'physical_inventory_import_wizard_view')],
3745+ 'target': 'new',
3746+ 'context': context or {},
3747+ }
3748+
3749+ def _set_action_for_all(self, cr, uid, ids, action, context=None):
3750+ lines = self.pool.get('physical.inventory.import.line.wizard')
3751+ line_ids = lines.search(cr, uid, [('parent_id', 'in', ids)])
3752+ lines.write(cr, uid, line_ids, {'action': action}, context=context)
3753+
3754+ def action_ignore_all(self, cr, uid, ids, context=None):
3755+ self._set_action_for_all(cr, uid, ids, 'ignore', context)
3756+ return self.no_action()
3757+
3758+ def action_count_all(self, cr, uid, ids, context=None):
3759+ self._set_action_for_all(cr, uid, ids, 'count', context)
3760+ return self.no_action()
3761+
3762+ def action_validate(self, cr, uid, ids, context=None):
3763+ lines = self.pool.get('physical.inventory.import.line.wizard')
3764+ line_ids = lines.search(cr, uid, [('parent_id', 'in', ids)])
3765+ items = lines.read(cr, uid, line_ids, ['line_id', 'action'])
3766+ # Search for undefined actions
3767+ for row in items:
3768+ if not row['action']:
3769+ break
3770+ else:
3771+ # Not found, all actions are defined
3772+ self.pool.get('physical.inventory').pre_process_discrepancies(cr, uid, items, context=context)
3773+ return self.close_action()
3774+ # Found, an action is missing on a line
3775+ return self.no_action()
3776+
3777+ @staticmethod
3778+ def close_action():
3779+ return {'type': 'ir.actions.act_window_close'}
3780+
3781+ @staticmethod
3782+ def no_action():
3783+ return {}
3784+
3785+ def get_view_by_name(self, cr, uid, name):
3786+ return self.pool.get('ir.model.data').get_object_reference(cr, uid, 'stock', name)[1]
3787+
3788+ def action_box(self, cr, uid, title, items=None, context=None):
3789+ result = {
3790+ 'name': title,
3791+ 'view_type': 'form',
3792+ 'view_mode': 'form',
3793+ 'res_model': self._name,
3794+ 'res_id': self.create(cr, uid, {'line_ids': [(0, 0, item) for item in items or []]}, context=context),
3795+ 'type': 'ir.actions.act_window',
3796+ 'view_id': [self.get_view_by_name(cr, uid, 'physical_inventory_action_box_wizard_view')],
3797+ 'target': 'new',
3798+ 'context': context or {},
3799+ }
3800+ return result
3801+
3802+
3803+PhysicalInventoryImportWizard()
3804+
3805+
3806+class PhysicalInventoryImportLineWizard(osv.osv_memory):
3807+ _name = "physical.inventory.import.line.wizard"
3808+ _description = "Physical inventory import line wizard"
3809+
3810+ ACTION_LIST = [
3811+ ('ignore', 'Ignore'),
3812+ ('count', 'Count as 0'),
3813+ ]
3814+
3815+ _columns = {
3816+ 'parent_id': fields.many2one('physical.inventory.import.wizard', string='Parent'),
3817+ 'line_id': fields.integer('Line ID', readonly=True),
3818+ 'message': fields.text('Message', readonly=True),
3819+ 'action': fields.selection(ACTION_LIST, string='Action')
3820+ }
3821+
3822+
3823+PhysicalInventoryImportLineWizard()
3824
3825=== added file 'bin/addons/stock/wizard/physical_inventory_import_view.xml'
3826--- bin/addons/stock/wizard/physical_inventory_import_view.xml 1970-01-01 00:00:00 +0000
3827+++ bin/addons/stock/wizard/physical_inventory_import_view.xml 2017-11-24 09:38:22 +0000
3828@@ -0,0 +1,38 @@
3829+<?xml version="1.0" encoding="utf-8"?>
3830+<openerp>
3831+ <data>
3832+ <record id="physical_inventory_import_wizard_view" model="ir.ui.view">
3833+ <field name="name">Importation Report</field>
3834+ <field name="model">physical.inventory.import.wizard</field>
3835+ <field name="type">form</field>
3836+ <field name="arch" type="xml">
3837+ <form>
3838+ <field name="message" nolabel="1" colspan="4"/>
3839+ <separator colspan="4"/>
3840+ <button string="Close" icon="gtk-ok" colspan="4" name="action_close" type="object"/>
3841+ </form>
3842+ </field>
3843+ </record>
3844+ <record id="physical_inventory_action_box_wizard_view" model="ir.ui.view">
3845+ <field name="name">Importation Report</field>
3846+ <field name="model">physical.inventory.import.wizard</field>
3847+ <field name="type">form</field>
3848+ <field name="priority">17</field>
3849+ <field name="arch" type="xml">
3850+ <form>
3851+ <field name="line_ids" nolabel="1" colspan="4">
3852+ <tree editable="bottom" hide_new_button="True" hide_delete_button="True">
3853+ <field name="message"/>
3854+ <field name="action"/>
3855+ </tree>
3856+ </field>
3857+ <button string="Count all as 0" icon="gtk-clear" colspan="2" name="action_count_all" type="object"/>
3858+ <button string="Ignore all" icon="gtk-close" colspan="2" name="action_ignore_all" type="object"/>
3859+ <separator colspan="4"/>
3860+ <button string="Validate" icon="gtk-apply" colspan="2" name="action_validate" type="object"/>
3861+ <button string="Cancel" icon="gtk-cancel" colspan="2" name="action_close" type="object"/>
3862+ </form>
3863+ </field>
3864+ </record>
3865+ </data>
3866+</openerp>
3867
3868=== added file 'bin/addons/stock/wizard/physical_inventory_select_products.py'
3869--- bin/addons/stock/wizard/physical_inventory_select_products.py 1970-01-01 00:00:00 +0000
3870+++ bin/addons/stock/wizard/physical_inventory_select_products.py 2017-11-24 09:38:22 +0000
3871@@ -0,0 +1,413 @@
3872+# -*- coding: utf-8 -*-
3873+##############################################################################
3874+#
3875+# OpenERP, Open Source Management Solution
3876+# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
3877+#
3878+# This program is free software: you can redistribute it and/or modify
3879+# it under the terms of the GNU Affero General Public License as
3880+# published by the Free Software Foundation, either version 3 of the
3881+# License, or (at your option) any later version.
3882+#
3883+# This program is distributed in the hope that it will be useful,
3884+# but WITHOUT ANY WARRANTY; without even the implied warranty of
3885+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
3886+# GNU Affero General Public License for more details.
3887+#
3888+# You should have received a copy of the GNU Affero General Public License
3889+# along with this program. If not, see <http://www.gnu.org/licenses/>.
3890+#
3891+##############################################################################
3892+
3893+from osv import fields, osv
3894+from tools.translate import _
3895+from datetime import datetime
3896+from dateutil.relativedelta import relativedelta
3897+
3898+# This will be a tuple ((1,"1 months" ), (2, "2 months"), ...)
3899+MOVED_IN_LAST_X_MONTHS = tuple([(i, "%s months" % str(i)) for i in range(1, 13)])
3900+
3901+
3902+class physical_inventory_select_products(osv.osv_memory):
3903+ _name = "physical.inventory.select.products"
3904+ _description = "Select product to consider for inventory, using filters"
3905+
3906+ _columns = {
3907+
3908+ 'inventory_id': fields.many2one('physical.inventory', 'Inventory', readonly=True),
3909+ 'full_inventory': fields.boolean('Full Inventory'),
3910+
3911+ # If this is a full inventory, we'll add products in stock at that
3912+ # location + products with recent moves at that location
3913+ 'recent_moves_months_fullinvo': fields.selection(MOVED_IN_LAST_X_MONTHS, "Moved in the last", select=True),
3914+
3915+ # For partial inventories :
3916+
3917+ # First filter is to select products either in stock, or with recent
3918+ # moves at that location
3919+ 'first_filter': fields.selection((('in_stock', "Products currently in stock at location"),
3920+ ('recent_movements', "Products with recent movement at location")),
3921+ "First filter", select=True),
3922+ 'recent_moves_months': fields.selection(MOVED_IN_LAST_X_MONTHS, "Moved in the last", select=True),
3923+
3924+ # Second filter is to select a family / product list / 'special care' products
3925+ 'second_filter': fields.selection((('all', "All"),
3926+ ('family', "All from a family"),
3927+ ('productlist', "All from a product list"),
3928+ ('specialcare', "KC/CS/DG")),
3929+ "Second filter", select=True),
3930+ 'kc': fields.boolean('Keep cool items'),
3931+ 'cs': fields.boolean('Controlled substances'),
3932+ 'dg': fields.boolean('Dangerous goods'),
3933+ 'product_list': fields.many2one('product.list', 'Product List', select=True),
3934+
3935+ # mandatory nomenclature levels
3936+ 'nomen_manda_0': fields.many2one('product.nomenclature', 'Main Type', readonly=False, select=1),
3937+ 'nomen_manda_1': fields.many2one('product.nomenclature', 'Group', readonly=False, select=1),
3938+ 'nomen_manda_2': fields.many2one('product.nomenclature', 'Family', readonly=False, select=1),
3939+ 'nomen_manda_3': fields.many2one('product.nomenclature', 'Root', readonly=False, select=1),
3940+
3941+ # Finally, we want to give the user some feedback about what's going
3942+ # to be imported
3943+ 'products_preview': fields.many2many('product.product', 'products_preview_rel', 'product_id',
3944+ 'physical_inventory_select_products_id', string="Products preview",
3945+ readonly=True),
3946+ }
3947+
3948+ def create(self, cr, user, vals, context=None):
3949+
3950+ context = context if context else {}
3951+
3952+ assert 'inventory_id' in vals
3953+ assert 'full_inventory' in vals
3954+
3955+ return super(physical_inventory_select_products, self).create(cr, user, vals, context=context)
3956+
3957+ #
3958+ # Nomenclature management
3959+ #
3960+ def onChangeSearchNomenclature(self, cr, uid, id, position, type,
3961+ nomen_manda_0,
3962+ nomen_manda_1,
3963+ nomen_manda_2,
3964+ nomen_manda_3,
3965+ num=True,
3966+ context=None):
3967+
3968+ context = context if context else {}
3969+
3970+ return self.pool.get('product.product') \
3971+ .onChangeSearchNomenclature(cr, uid, id, position, type,
3972+ nomen_manda_0,
3973+ nomen_manda_1,
3974+ nomen_manda_2,
3975+ nomen_manda_3,
3976+ num=num,
3977+ context=context)
3978+
3979+ def get_nomen(self, cr, uid, id, field):
3980+ return self.pool.get('product.nomenclature').get_nomen(cr, uid, self, id, field)
3981+
3982+ #
3983+ # END Nomenclature management
3984+ #
3985+
3986+
3987+ #
3988+ # When clicking 'Refresh'
3989+ #
3990+ def refresh_products(self, cr, uid, wizard_ids, context=None):
3991+ context = context if context else {}
3992+
3993+ def read_single(model, id_, column):
3994+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
3995+
3996+ def read_many(model, ids, columns):
3997+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
3998+
3999+ # Get this wizard...
4000+ assert len(wizard_ids) == 1
4001+ wizard_id = wizard_ids[0]
4002+
4003+ inventory_id = read_single(self._name, wizard_id, "inventory_id")
4004+
4005+ # Get the selected options
4006+ w = read_many(self._name, [wizard_id], ['inventory_id',
4007+ 'full_inventory',
4008+ 'recent_moves_months_fullinvo',
4009+ 'first_filter',
4010+ 'recent_moves_months',
4011+ 'second_filter',
4012+ 'kc',
4013+ 'cs',
4014+ 'dg',
4015+ 'product_list',
4016+ 'nomen_manda_0',
4017+ 'nomen_manda_1',
4018+ 'nomen_manda_2',
4019+ 'nomen_manda_3'])[0]
4020+
4021+ # Find the location of the inventory
4022+ location_id = read_single('physical.inventory', w["inventory_id"], "location_id")[0]
4023+
4024+ # Full inventory case
4025+ if w["full_inventory"]:
4026+
4027+ # Get products in stock at that location, or recently moved
4028+ products_in_stock = self.get_products_in_stock_at_location(cr, uid, location_id, context=context)
4029+ products_recently_moved = self.get_products_with_recent_moves_at_location(cr, uid, location_id,
4030+ w['recent_moves_months_fullinvo'],
4031+ context=context)
4032+ products = products_in_stock.union(products_recently_moved)
4033+
4034+ # Show them in the preview
4035+ self.update_product_preview(cr, uid, wizard_id, products, context=context)
4036+
4037+ # Partial inventory case
4038+ else:
4039+
4040+ # Handle the first filter
4041+ if w["first_filter"] == "in_stock":
4042+ products = self.get_products_in_stock_at_location(cr, uid, location_id, context=context)
4043+ elif w["first_filter"] == "recent_movements":
4044+ products = self.get_products_with_recent_moves_at_location(cr, uid, location_id,
4045+ w['recent_moves_months'], context=context)
4046+ else:
4047+ # Does not happens
4048+ pass
4049+
4050+ # Handle the second filter
4051+ if w['second_filter'] == 'productlist':
4052+ product_list_id = read_single(self._name, wizard_id, "product_list")
4053+ products = self.filter_products_with_product_list(cr, uid, products, product_list_id, context=context)
4054+
4055+ elif w['second_filter'] == 'specialcare':
4056+ special_care_criterias = [c for c in ['kc', 'cs', 'dg'] if w[c]]
4057+ products = self.filter_products_with_special_care(cr, uid, products, special_care_criterias,
4058+ context=context)
4059+
4060+ elif w['second_filter'] == 'family':
4061+ nomenclature = {name: w[name] for name in ['nomen_manda_0',
4062+ 'nomen_manda_1',
4063+ 'nomen_manda_2',
4064+ 'nomen_manda_3'
4065+ ] if w[name]}
4066+ products = self.filter_products_with_nomenclature(cr, uid, products, nomenclature, context=context)
4067+
4068+ else:
4069+ # Nothing to do, keep all the products found with first filter
4070+ pass
4071+
4072+ # Show them in the preview
4073+ self.update_product_preview(cr, uid, wizard_id, products, context=context)
4074+ return {}
4075+
4076+
4077+ def update_product_preview(self, cr, uid, wizard_id, product_ids, context=None):
4078+ context = context if context else {}
4079+
4080+ assert isinstance(wizard_id, int)
4081+ assert isinstance(product_ids, list) or isinstance(product_ids, set)
4082+
4083+ # '6' is the code for 'replace all'
4084+ vals = {"products_preview": [(6, 0, list(product_ids))]}
4085+
4086+ self.write(cr, uid, [wizard_id], vals, context=context)
4087+
4088+ def get_moves_at_location(self, cr, uid, location_id, context=None):
4089+ context = context if context else {}
4090+
4091+ def read_many(model, ids, columns):
4092+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
4093+
4094+ def search(model, domain):
4095+ return self.pool.get(model).search(cr, uid, domain, context=context)
4096+
4097+ assert isinstance(location_id, int)
4098+
4099+ # Get all the moves for in/out of that location
4100+ from_or_to_location = ['&', '|',
4101+ ('location_id', 'in', [location_id]),
4102+ ('location_dest_id', 'in', [location_id]),
4103+ ('state', '=', 'done')]
4104+
4105+ moves_at_location_ids = search("stock.move", from_or_to_location)
4106+ moves_at_location = read_many("stock.move",
4107+ moves_at_location_ids,
4108+ ["product_id",
4109+ "date",
4110+ "product_qty",
4111+ "location_id",
4112+ "location_dest_id"])
4113+
4114+ return moves_at_location
4115+
4116+ def get_products_in_stock_at_location(self, cr, uid, location_id, context=None):
4117+ context = context if context else {}
4118+
4119+ assert isinstance(location_id, int)
4120+
4121+ moves_at_location = self.get_moves_at_location(cr, uid, location_id, context=None)
4122+
4123+ # Init stock at 0 for products
4124+ stocks = {}
4125+ for product_id in set([m["product_id"][0] for m in moves_at_location]):
4126+ stocks[product_id] = 0.0
4127+
4128+ # Sum all lines
4129+ for move in moves_at_location:
4130+
4131+ product_id = move["product_id"][0]
4132+ product_qty = move["product_qty"]
4133+
4134+ move_out = (move["location_id"][0] == location_id)
4135+ move_in = (move["location_dest_id"][0] == location_id)
4136+
4137+ if move_in:
4138+ stocks[product_id] += product_qty
4139+ elif move_out:
4140+ stocks[product_id] -= product_qty
4141+ else:
4142+ # This shouldnt happen
4143+ pass
4144+
4145+ # Keep only products for which stock != 0 (including negative ones)
4146+ products_in_stock = set([product_id
4147+ for product_id, stock in stocks.items()
4148+ if stock != 0.0])
4149+
4150+ return products_in_stock
4151+
4152+ def get_products_with_recent_moves_at_location(self, cr, uid, location_id, recent_moves_months, context=None):
4153+ context = context if context else {}
4154+
4155+ assert isinstance(location_id, int)
4156+ assert isinstance(recent_moves_months, int)
4157+
4158+ moves_at_location = self.get_moves_at_location(cr, uid, location_id, context=None)
4159+
4160+ several_months_ago = datetime.today() + relativedelta(months=-recent_moves_months)
4161+
4162+ # Keep only the product ids related to moves during the last few months
4163+ recently_moved_products = set()
4164+ for move in moves_at_location:
4165+ # Parse the move's date
4166+ move_date = datetime.strptime(move['date'], '%Y-%m-%d %H:%M:%S')
4167+ if move_date > several_months_ago:
4168+ product_id = move['product_id'][0]
4169+ recently_moved_products.add(product_id)
4170+
4171+ return recently_moved_products
4172+
4173+ def filter_products_with_special_care(self, cr, uid, product_ids, special_care_criterias, context=None):
4174+ context = context if context else {}
4175+
4176+ def search(model, domain):
4177+ return self.pool.get(model).search(cr, uid, domain, context=context)
4178+
4179+ assert isinstance(special_care_criterias, list)
4180+ assert isinstance(product_ids, list) or isinstance(product_ids, set)
4181+
4182+ domain_filter = [('id', 'in', list(product_ids))]
4183+
4184+ # To convert wizard option to column name in product.product
4185+ special_care_column_name = {'kc': 'is_kc',
4186+ 'cs': 'is_cs',
4187+ 'dg': 'is_dg'}
4188+
4189+ # Add special care criterias to the domain
4190+ for criteria in special_care_criterias:
4191+ column_name = special_care_column_name[criteria]
4192+ domain_filter = ['&'] + domain_filter + [(column_name, '=', True)]
4193+
4194+ # Perform the search/fitlering
4195+ products_filtered = search('product.product', domain_filter)
4196+
4197+ return products_filtered
4198+
4199+ def filter_products_with_nomenclature(self, cr, uid, product_ids, nomenclature, context=None):
4200+ context = context if context else {}
4201+
4202+ def search(model, domain):
4203+ return self.pool.get(model).search(cr, uid, domain, context=context)
4204+
4205+ assert isinstance(nomenclature, dict)
4206+ assert isinstance(product_ids, list) or isinstance(product_ids, set)
4207+
4208+ domain_filter = [('id', 'in', list(product_ids))]
4209+
4210+ # Add nomenclature to the domain
4211+ for name, value in nomenclature.items():
4212+ domain_filter = ['&'] + domain_filter + [(name, '=', value)]
4213+
4214+ # Perform the search/fitlering
4215+ products_filtered = search('product.product', domain_filter)
4216+
4217+ return products_filtered
4218+
4219+ def filter_products_with_product_list(self, cr, uid, product_ids, product_list_id, context=None):
4220+ context = context if context else {}
4221+
4222+ def search(model, domain):
4223+ return self.pool.get(model).search(cr, uid, domain, context=context)
4224+
4225+ def read_many(model, ids, columns):
4226+ return self.pool.get(model).read(cr, uid, ids, columns, context=context)
4227+
4228+ # Get all the product ids in the list
4229+ product_lines_in_list = search('product.list.line',
4230+ [('list_id', '=', product_list_id)])
4231+
4232+ product_ids_in_list = read_many('product.list.line',
4233+ product_lines_in_list,
4234+ ['name'])
4235+
4236+ product_ids_in_list = set([l['name'][0] for l in product_ids_in_list])
4237+
4238+ # Now use these products in list to filter the products in input
4239+ product_ids_in_list = product_ids_in_list.intersection(product_ids)
4240+
4241+ return product_ids_in_list
4242+
4243+ #
4244+ # When clicking 'Add product'
4245+ #
4246+ def add_products(self, cr, uid, wizard_ids, context=None):
4247+ context = context if context else {}
4248+
4249+ # Get this wizard...
4250+ assert len(wizard_ids) == 1
4251+ wizard_id = wizard_ids[0]
4252+
4253+ self.refresh_products(cr, uid, [wizard_id], context=context)
4254+ self.update_products_in_inventory(cr, uid, wizard_id, context=context)
4255+
4256+ return {'type': 'ir.actions.act_window_close'}
4257+
4258+ def update_products_in_inventory(self, cr, uid, wizard_id, context=None):
4259+ context = context if context else {}
4260+
4261+ def read_single(model, id_, column):
4262+ return self.pool.get(model).read(cr, uid, [id_], [column], context=context)[0][column]
4263+
4264+ def write(model, id_, vals):
4265+ return self.pool.get(model).write(cr, uid, [id_], vals, context=context)
4266+
4267+ assert isinstance(wizard_id, int)
4268+
4269+ inventory_id = read_single(self._name, wizard_id, "inventory_id")
4270+ product_ids = read_single(self._name, wizard_id, "products_preview")
4271+
4272+ # Redo a search to force order according to default order
4273+ previously_selected_product_ids = read_single("physical.inventory", inventory_id, "product_ids")
4274+ product_ids = previously_selected_product_ids + product_ids
4275+ product_ids = self.pool.get("product.product").search(cr, uid, [("id", 'in', product_ids)], context=context)
4276+
4277+ # '6' is the code for 'replace all'
4278+ vals = {'product_ids': [(6, 0, product_ids)]}
4279+ write('physical.inventory', inventory_id, vals)
4280+
4281+
4282+physical_inventory_select_products()
4283+
4284+# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
4285
4286=== added file 'bin/addons/stock/wizard/physical_inventory_select_products_view.xml'
4287--- bin/addons/stock/wizard/physical_inventory_select_products_view.xml 1970-01-01 00:00:00 +0000
4288+++ bin/addons/stock/wizard/physical_inventory_select_products_view.xml 2017-11-24 09:38:22 +0000
4289@@ -0,0 +1,74 @@
4290+<?xml version="1.0" encoding="utf-8"?>
4291+<openerp>
4292+ <data>
4293+ <record id="physical_inventory_select_products" model="ir.ui.view">
4294+ <field name="name">Select Products</field>
4295+ <field name="model">physical.inventory.select.products</field>
4296+ <field name="type">form</field>
4297+ <field name="arch" type="xml">
4298+ <form string="Select Products">
4299+ <field name="full_inventory" invisible="1"/>
4300+ <group col="4" colspan="4" attrs="{'invisible':[('full_inventory', '=', False)]}">
4301+ <separator string="Full inventory" />
4302+ <group col="1" colspan="4">
4303+ <html>
4304+ <h3 style="text-align: center; margin: 13px"><translate>This is a full inventory. All products currently in stock and all products with movements in the last X months will be added.</translate></h3>
4305+ </html>
4306+ </group>
4307+ <field name="recent_moves_months_fullinvo" colspan="1" attrs="{'required':[('full_inventory','=',True)]}"/>
4308+ </group>
4309+ <group col="4" colspan="4" attrs="{'invisible':[('full_inventory', '=', True)]}">
4310+ <separator string="Partial inventory" />
4311+ <group col="2" colspan="2">
4312+ <field name="first_filter" attrs="{'required':[('full_inventory', '=', False)]}" />
4313+ <field name="recent_moves_months" attrs="{'invisible':[('first_filter','!=','recent_movements')],
4314+ 'required':[('first_filter', '=','recent_movements')]}" colspan="2"/>
4315+ </group>
4316+ <group col="2" colspan="2">
4317+ <field name="second_filter" attrs="{'required':[('full_inventory', '=', False)]}"/>
4318+ <field name="product_list" attrs="{'invisible':[('second_filter','!=','productlist')], 'required':[('second_filter','=','productlist')]}" />
4319+ <field name="kc" attrs="{'invisible':[('second_filter','!=','specialcare')]}" />
4320+ <field name="cs" attrs="{'invisible':[('second_filter','!=','specialcare')]}" />
4321+ <field name="dg" attrs="{'invisible':[('second_filter','!=','specialcare')]}" />
4322+ <group col="2" colspan="2" attrs="{'invisible':[('second_filter','!=','family')] }">
4323+ <field name="nomen_manda_0" widget="selection"
4324+ domain="[('level', '=', '0'), ('type', '=', 'mandatory')]"
4325+ on_change="onChangeSearchNomenclature(0, 'mandatory', nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, False)"
4326+ attrs="{'required':[('second_filter','=','family')]}"/>
4327+ <field name="nomen_manda_1" widget="selection" get_selection="get_nomen"
4328+ domain="[('level', '=', '1'), ('type', '=', 'mandatory')]"
4329+ on_change="onChangeSearchNomenclature(1, 'mandatory', nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, False)"
4330+ />
4331+ <field name="nomen_manda_2" widget="selection" get_selection="get_nomen"
4332+ domain="[('level', '=', '2'), ('type', '=', 'mandatory'), ('category_id', '!=', False)]"
4333+ on_change="onChangeSearchNomenclature(2, 'mandatory', nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, False)"
4334+ />
4335+ <field name="nomen_manda_3" widget="selection" get_selection="get_nomen"
4336+ domain="[('level', '=', '3'), ('type', '=', 'mandatory')]"
4337+ on_change="onChangeSearchNomenclature(3, 'mandatory', nomen_manda_0, nomen_manda_1, nomen_manda_2, nomen_manda_3, False)"
4338+ />
4339+ </group>
4340+ </group>
4341+ </group>
4342+ <separator colspan="4" string="Preview" />
4343+ <button name="refresh_products" string="Refresh" icon="gtk-refresh" colspan="4" type="object" />
4344+ <field colspan="4" name="products_preview" nolabel="1">
4345+ <tree string="Products">
4346+ <field name="default_code" />
4347+ <field name="name" />
4348+ <field name="uom_id" />
4349+ <field name="batch_management" string="BN mandatory" />
4350+ <field name="perishable" string="ED mandatory" />
4351+ <field name="is_kc" string="KC" />
4352+ <field name="is_dg" string="DG" />
4353+ <field name="is_cs" string="CS" />
4354+ </tree>
4355+ </field>
4356+ <separator colspan="4" string="Actions" />
4357+ <button special="cancel" string="Cancel" icon="gtk-cancel" />
4358+ <button name="add_products" string="Add products" icon="terp-check" colspan="3" type="object" />
4359+ </form>
4360+ </field>
4361+ </record>
4362+ </data>
4363+</openerp>
4364
4365=== modified file 'bin/addons/stock_override/wizard/stock_card_wizard.py'
4366--- bin/addons/stock_override/wizard/stock_card_wizard.py 2017-08-23 14:10:02 +0000
4367+++ bin/addons/stock_override/wizard/stock_card_wizard.py 2017-11-24 09:38:22 +0000
4368@@ -97,7 +97,10 @@
4369 loc_obj = self.pool.get('stock.location')
4370 product_obj = self.pool.get('product.product')
4371 line_obj = self.pool.get('stock.card.wizard.line')
4372- inv_line_obj = self.pool.get('stock.inventory.line')
4373+ pi_line_obj = self.pool.get('physical.inventory.counting')
4374+ # 'Old' physical inventories
4375+ oldinv_line_obj = self.pool.get('stock.inventory.line')
4376+
4377
4378 if not context:
4379 context = {}
4380@@ -131,6 +134,7 @@
4381 ('prodlot_id', '=', prodlot_id),
4382 ('state', '=', 'done')]
4383
4384+ # "Old" physical inventory
4385 inv_dom = [
4386 ('product_id', '=', product.id),
4387 ('prod_lot_id', '=', prodlot_id),
4388@@ -138,28 +142,38 @@
4389 ('inventory_id.state', '=', 'done')
4390 ]
4391
4392+ pi_counting_dom = [
4393+ ('product_id', '=', product.id),
4394+ ('prod_lot_id', '=', prodlot_id),
4395+ ('discrepancy', '=', False),
4396+ ('inventory_id.state', 'in', ['confirmed', 'closed'])
4397+ ]
4398+
4399 if card.from_date:
4400 domain.append(('date', '>=', card.from_date))
4401 inv_dom.append(('inventory_id.date_done', '>=', card.from_date))
4402+ pi_counting_dom.append(('inventory_id.date_done', '>=', card.from_date))
4403
4404 if card.to_date:
4405 domain.append(('date', '<=', card.to_date))
4406 inv_dom.append(('inventory_id.date_done', '<=', card.to_date + ' 23:59:00'))
4407+ pi_counting_dom.append(('inventory_id.date_done', '<=', card.to_date + ' 23:59:00'))
4408
4409 if location_id:
4410 domain.extend(['|',
4411 ('location_id', 'child_of', location_id),
4412 ('location_dest_id', 'child_of', location_id)])
4413 inv_dom.append(('location_id', 'child_of', location_id))
4414+ pi_counting_dom.append(('inventory_id.location_id', 'child_of', location_id))
4415 else:
4416 domain.extend(['|',
4417 ('location_id.usage', 'in', location_usage),
4418 ('location_dest_id.usage', 'in', location_usage)])
4419
4420-
4421- inv_line_ids = inv_line_obj.search(cr, uid, inv_dom, context=context)
4422+ # Lines from "old" physical inventories
4423+ inv_line_ids = oldinv_line_obj.search(cr, uid, inv_dom, context=context)
4424 inv_line_to_add = {}
4425- for line in inv_line_obj.browse(cr, uid, inv_line_ids, context=context):
4426+ for line in oldinv_line_obj.browse(cr, uid, inv_line_ids, context=context):
4427 inv_line_to_add.setdefault(line.inventory_id.date_done, []).append({
4428 'card_id': ids[0],
4429 'date_done': line.inventory_id.date_done,
4430@@ -172,10 +186,28 @@
4431 'notes': '',
4432 })
4433
4434-
4435 inv_line_dates = inv_line_to_add.keys()
4436 inv_line_dates.sort()
4437
4438+ pi_counting_line_ids = pi_line_obj.search(cr, uid, pi_counting_dom, context=context)
4439+ pi_counting_line_to_add = {}
4440+ for line in pi_line_obj.browse(cr, uid, pi_counting_line_ids, context=context):
4441+ pi_counting_line_to_add.setdefault(line.inventory_id.date_done, []).append({
4442+ 'card_id': ids[0],
4443+ 'date_done': line.inventory_id.date_done,
4444+ 'doc_ref': line.inventory_id.name,
4445+ 'origin': False,
4446+ 'qty_in': 0,
4447+ 'qty_out': 0,
4448+ 'balance': 0,
4449+ 'src_dest': line.product_id.property_stock_inventory and line.product_id.property_stock_inventory.name or False,
4450+ 'notes': '',
4451+ })
4452+
4453+ pi_counting_line_dates = pi_counting_line_to_add.keys()
4454+ pi_counting_line_dates.sort()
4455+
4456+
4457 # Create one line per stock move
4458 move_ids = move_obj.search(cr, uid, domain,order='date asc',
4459 context=context)
4460@@ -200,6 +232,11 @@
4461 new_line['balance'] = initial_stock
4462 line_obj.create(cr, uid, new_line, context=context)
4463
4464+ while pi_counting_line_dates and pi_counting_line_dates[0] < move.date:
4465+ inv_data = pi_counting_line_dates.pop(0)
4466+ for new_line in pi_counting_line_to_add[inv_data]:
4467+ new_line['balance'] = initial_stock
4468+ line_obj.create(cr, uid, new_line, context=context)
4469
4470 in_qty, out_qty = 0.00, 0.00
4471 move_location = False
4472@@ -252,6 +289,11 @@
4473 new_line['balance'] = initial_stock
4474 line_obj.create(cr, uid, new_line, context=context)
4475
4476+ for pi_counting_date in pi_counting_line_dates:
4477+ for new_line in pi_counting_line_to_add[pi_counting_date]:
4478+ new_line['balance'] = initial_stock
4479+ line_obj.create(cr, uid, new_line, context=context)
4480+
4481 self.write(cr, uid, [ids[0]], {'available_stock': initial_stock},
4482 context=context)
4483
4484
4485=== modified file 'bin/addons/useability_dashboard_and_menu/menu/warehouse_menu.xml'
4486--- bin/addons/useability_dashboard_and_menu/menu/warehouse_menu.xml 2017-05-22 14:32:14 +0000
4487+++ bin/addons/useability_dashboard_and_menu/menu/warehouse_menu.xml 2017-11-24 09:38:22 +0000
4488@@ -107,17 +107,16 @@
4489 sequence="23"
4490 action="mission_stock.mission_stock_wizard_action" />
4491
4492- <menuitem name="Physical Inventories"
4493- action="stock.action_inventory_form"
4494- id="stock.menu_action_inventory_form"
4495- parent="stock.menu_stock_inventory_control"
4496- sequence="24"/>
4497-
4498 <menuitem name="Last Product Inventories"
4499 parent="stock.menu_stock_inventory_control"
4500 action="stock.action_stock_line_date"
4501 id="stock.menu_report_stock_line_date"
4502 sequence="25"/>
4503-
4504+
4505+ <menuitem name="Previous Physical Inventories"
4506+ action="stock.action_inventory_form"
4507+ id="stock.menu_action_inventory_form"
4508+ parent="stock.menu_stock_inventory_control"
4509+ sequence="30"/>
4510 </data>
4511 </openerp>
4512
4513=== modified file 'bin/osv/orm.py'
4514--- bin/osv/orm.py 2017-10-27 08:30:22 +0000
4515+++ bin/osv/orm.py 2017-11-24 09:38:22 +0000
4516@@ -3618,6 +3618,8 @@
4517 res2 = self._columns[val[0]].get(cr, self, ids, val, user, context=context, values=res)
4518 for pos in val:
4519 for record in res:
4520+ if not record['id'] in res2:
4521+ raise Exception("Could not process key %s" % key)
4522 if isinstance(res2[record['id']], str): res2[record['id']] = eval(res2[record['id']]) #TOCHECK : why got string instend of dict in python2.6
4523 multi_fields = res2.get(record['id'],{})
4524 if multi_fields:
4525@@ -3893,6 +3895,7 @@
4526 """
4527 if not ids:
4528 return True
4529+ assert isinstance(vals, dict), "orm.write() expects a dictionnary as 4th argument!"
4530 readonly = None
4531 for field in vals.copy():
4532 fobj = None

Subscribers

People subscribed via source and target branches

to all changes: