summaryrefslogtreecommitdiff
path: root/addons/point_of_sale/tests/common.py
blob: cf51f9fe4e3f84f771882eda138a27186b9ce9fe (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
# -*- coding: utf-8 -*-
from random import randint

from odoo import fields, tools
from odoo.addons.stock_account.tests.test_anglo_saxon_valuation_reconciliation_common import ValuationReconciliationTestCommon
from odoo.tests.common import SavepointCase, Form
from odoo.tests import tagged


@tagged('post_install', '-at_install')
class TestPointOfSaleCommon(ValuationReconciliationTestCommon):

    @classmethod
    def setUpClass(cls, chart_template_ref=None):
        super().setUpClass(chart_template_ref=chart_template_ref)

        cls.company_data['company'].write({
            'point_of_sale_update_stock_quantities': 'real',
        })

        cls.AccountBankStatement = cls.env['account.bank.statement']
        cls.AccountBankStatementLine = cls.env['account.bank.statement.line']
        cls.PosMakePayment = cls.env['pos.make.payment']
        cls.PosOrder = cls.env['pos.order']
        cls.PosSession = cls.env['pos.session']
        cls.company = cls.company_data['company']
        cls.product3 = cls.env['product.product'].create({
            'name': 'Product 3',
            'list_price': 450,
        })
        cls.product4 = cls.env['product.product'].create({
            'name': 'Product 4',
            'list_price': 750,
        })
        cls.partner1 = cls.env['res.partner'].create({'name': 'Partner 1'})
        cls.partner4 = cls.env['res.partner'].create({'name': 'Partner 4'})
        cls.pos_config = cls.env['pos.config'].create({
            'name': 'Main',
            'journal_id': cls.company_data['default_journal_sale'].id,
            'invoice_journal_id': cls.company_data['default_journal_sale'].id,
        })
        cls.led_lamp = cls.env['product.product'].create({
            'name': 'LED Lamp',
            'available_in_pos': True,
            'list_price': 0.90,
        })
        cls.whiteboard_pen = cls.env['product.product'].create({
            'name': 'Whiteboard Pen',
            'available_in_pos': True,
            'list_price': 1.20,
        })
        cls.newspaper_rack = cls.env['product.product'].create({
            'name': 'Newspaper Rack',
            'available_in_pos': True,
            'list_price': 1.28,
        })
        cls.cash_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Cash',
            'receivable_account_id': cls.company_data['default_account_receivable'].id,
            'is_cash_count': True,
            'cash_journal_id': cls.company_data['default_journal_cash'].id,
            'company_id': cls.env.company.id,
        })
        cls.bank_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Bank',
            'receivable_account_id': cls.company_data['default_account_receivable'].id,
            'is_cash_count': False,
            'company_id': cls.env.company.id,
        })
        cls.credit_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Credit',
            'receivable_account_id': cls.company_data['default_account_receivable'].id,
            'split_transactions': True,
            'company_id': cls.env.company.id,
        })
        cls.pos_config.write({'payment_method_ids': [(4, cls.credit_payment_method.id), (4, cls.bank_payment_method.id), (4, cls.cash_payment_method.id)]})

        # Create POS journal
        cls.pos_config.journal_id = cls.env['account.journal'].create({
            'type': 'sale',
            'name': 'Point of Sale - Test',
            'code': 'POSS - Test',
            'company_id': cls.env.company.id,
            'sequence': 20
        })

        # create a VAT tax of 10%, included in the public price
        Tax = cls.env['account.tax']
        account_tax_10_incl = Tax.create({
            'name': 'VAT 10 perc Incl',
            'amount_type': 'percent',
            'amount': 10.0,
            'price_include': True,
        })

        # assign this 10 percent tax on the [PCSC234] PC Assemble SC234 product
        # as a sale tax
        cls.product3.taxes_id = [(6, 0, [account_tax_10_incl.id])]

        # create a VAT tax of 5%, which is added to the public price
        account_tax_05_incl = Tax.create({
            'name': 'VAT 5 perc Incl',
            'amount_type': 'percent',
            'amount': 5.0,
            'price_include': False,
        })

        # create a second VAT tax of 5% but this time for a child company, to
        # ensure that only product taxes of the current session's company are considered
        #(this tax should be ignore when computing order's taxes in following tests)
        account_tax_05_incl_chicago = Tax.create({
            'name': 'VAT 05 perc Excl (US)',
            'amount_type': 'percent',
            'amount': 5.0,
            'price_include': False,
            'company_id': cls.company_data_2['company'].id,
        })

        cls.product4.company_id = False
        # I assign those 5 percent taxes on the PCSC349 product as a sale taxes
        cls.product4.write(
            {'taxes_id': [(6, 0, [account_tax_05_incl.id, account_tax_05_incl_chicago.id])]})

        # Set account_id in the generated repartition lines. Automatically, nothing is set.
        invoice_rep_lines = (account_tax_05_incl | account_tax_10_incl).mapped('invoice_repartition_line_ids')
        refund_rep_lines = (account_tax_05_incl | account_tax_10_incl).mapped('refund_repartition_line_ids')

        # Expense account, should just be something else than receivable/payable
        (invoice_rep_lines | refund_rep_lines).write({'account_id': cls.company_data['default_account_tax_sale'].id})


@tagged('post_install', '-at_install')
class TestPoSCommon(ValuationReconciliationTestCommon):
    """ Set common values for different special test cases.

    The idea is to set up common values here for the tests
    and implement different special scenarios by inheriting
    this class.
    """

    @classmethod
    def setUpClass(cls, chart_template_ref=None):
        super().setUpClass(chart_template_ref=chart_template_ref)

        cls.company_data['company'].write({
            'point_of_sale_update_stock_quantities': 'real',
        })

        # Set basic defaults
        cls.company = cls.company_data['company']
        cls.pos_sale_journal = cls.env['account.journal'].create({
            'type': 'sale',
            'name': 'Point of Sale Test',
            'code': 'POST',
            'company_id': cls.company.id,
            'sequence': 20
        })
        cls.invoice_journal = cls.company_data['default_journal_sale']
        cls.receivable_account = cls.company_data['default_account_receivable']
        cls.tax_received_account = cls.company_data['default_account_tax_sale']
        cls.company.account_default_pos_receivable_account_id = cls.env['account.account'].create({
            'code': 'X1012 - POS',
            'name': 'Debtors - (POS)',
            'reconcile': True,
            'user_type_id': cls.env.ref('account.data_account_type_receivable').id,
        })
        cls.pos_receivable_account = cls.company.account_default_pos_receivable_account_id
        cls.other_receivable_account = cls.env['account.account'].create({
            'name': 'Other Receivable',
            'code': 'RCV00' ,
            'user_type_id': cls.env['account.account.type'].create({'name': 'RCV type', 'type': 'receivable', 'internal_group': 'asset'}).id,
            'internal_group': 'asset',
            'reconcile': True,
        })

        # company_currency can be different from `base.USD` depending on the localization installed
        cls.company_currency = cls.company.currency_id
        # other_currency is a currency different from the company_currency
        # sometimes company_currency is different from USD, so handle appropriately.
        cls.other_currency = cls.currency_data['currency']

        cls.currency_pricelist = cls.env['product.pricelist'].create({
            'name': 'Public Pricelist',
            'currency_id': cls.company_currency.id,
        })
        # Set Point of Sale configurations
        # basic_config
        #   - derived from 'point_of_sale.pos_config_main' with added invoice_journal_id and credit payment method.
        # other_currency_config
        #   - pos.config set to have currency different from company currency.
        cls.basic_config = cls._create_basic_config()
        cls.other_currency_config = cls._create_other_currency_config()

        # Set product categories
        # categ_basic
        #   - just the plain 'product.product_category_all'
        # categ_anglo
        #   - product category with fifo and real_time valuations
        #   - used for checking anglo saxon accounting behavior
        cls.categ_basic = cls.env.ref('product.product_category_all')
        cls.env.company.anglo_saxon_accounting = True
        cls.categ_anglo = cls._create_categ_anglo()

        # other basics
        cls.sale_account = cls.categ_basic.property_account_income_categ_id
        cls.other_sale_account = cls.env['account.account'].search([
            ('company_id', '=', cls.company.id),
            ('user_type_id', '=', cls.env.ref('account.data_account_type_revenue').id),
            ('id', '!=', cls.sale_account.id)
        ], limit=1)

        # Set customers
        cls.customer = cls.env['res.partner'].create({'name': 'Test Customer'})
        cls.other_customer = cls.env['res.partner'].create({'name': 'Other Customer', 'property_account_receivable_id': cls.other_receivable_account.id})

        # Set taxes
        # cls.taxes => dict
        #   keys: 'tax7', 'tax10'(price_include=True), 'tax_group_7_10'
        cls.taxes = cls._create_taxes()

        cls.stock_location_components = cls.env["stock.location"].create({
            'name': 'Shelf 1',
            'location_id': cls.company_data['default_warehouse'].lot_stock_id.id,
        })

    #####################
    ## private methods ##
    #####################

    @classmethod
    def _create_basic_config(cls):
        new_config = Form(cls.env['pos.config'])
        new_config.name = 'PoS Shop Test'
        new_config.module_account = True
        new_config.invoice_journal_id = cls.invoice_journal
        new_config.journal_id = cls.pos_sale_journal
        new_config.available_pricelist_ids.clear()
        new_config.available_pricelist_ids.add(cls.currency_pricelist)
        new_config.pricelist_id = cls.currency_pricelist
        config = new_config.save()
        cash_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Cash',
            'receivable_account_id': cls.pos_receivable_account.id,
            'is_cash_count': True,
            'cash_journal_id': cls.company_data['default_journal_cash'].id,
            'company_id': cls.env.company.id,
        })
        bank_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Bank',
            'receivable_account_id': cls.pos_receivable_account.id,
            'is_cash_count': False,
            'company_id': cls.env.company.id,
        })
        cash_split_pm = cls.env['pos.payment.method'].create({
            'name': 'Split (Cash) PM',
            'receivable_account_id': cls.pos_receivable_account.id,
            'split_transactions': True,
            'is_cash_count': True,
            'cash_journal_id': cls.company_data['default_journal_cash'].id,
        })
        bank_split_pm = cls.env['pos.payment.method'].create({
            'name': 'Split (Bank) PM',
            'receivable_account_id': cls.pos_receivable_account.id,
            'split_transactions': True,
        })
        config.write({'payment_method_ids': [(4, cash_split_pm.id), (4, bank_split_pm.id), (4, cash_payment_method.id), (4, bank_payment_method.id)]})
        return config

    @classmethod
    def _create_other_currency_config(cls):
        (cls.other_currency.rate_ids | cls.company_currency.rate_ids).unlink()
        cls.env['res.currency.rate'].create({
            'rate': 0.5,
            'currency_id': cls.other_currency.id,
        })
        other_cash_journal = cls.env['account.journal'].create({
            'name': 'Cash Other',
            'type': 'cash',
            'company_id': cls.company.id,
            'code': 'CSHO',
            'sequence': 10,
            'currency_id': cls.other_currency.id
        })
        other_invoice_journal = cls.env['account.journal'].create({
            'name': 'Customer Invoice Other',
            'type': 'sale',
            'company_id': cls.company.id,
            'code': 'INVO',
            'sequence': 11,
            'currency_id': cls.other_currency.id
        })
        other_sales_journal = cls.env['account.journal'].create({
            'name':'PoS Sale Other',
            'type': 'sale',
            'code': 'POSO',
            'company_id': cls.company.id,
            'sequence': 12,
            'currency_id': cls.other_currency.id
        })
        other_pricelist = cls.env['product.pricelist'].create({
            'name': 'Public Pricelist Other',
            'currency_id': cls.other_currency.id,
        })
        other_cash_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Cash Other',
            'receivable_account_id': cls.pos_receivable_account.id,
            'is_cash_count': True,
            'cash_journal_id': other_cash_journal.id,
        })
        other_bank_payment_method = cls.env['pos.payment.method'].create({
            'name': 'Bank Other',
            'receivable_account_id': cls.pos_receivable_account.id,
        })

        new_config = Form(cls.env['pos.config'])
        new_config.name = 'Shop Other'
        new_config.invoice_journal_id = other_invoice_journal
        new_config.journal_id = other_sales_journal
        new_config.use_pricelist = True
        new_config.available_pricelist_ids.clear()
        new_config.available_pricelist_ids.add(other_pricelist)
        new_config.pricelist_id = other_pricelist
        new_config.payment_method_ids.clear()
        new_config.payment_method_ids.add(other_cash_payment_method)
        new_config.payment_method_ids.add(other_bank_payment_method)
        config = new_config.save()
        return config

    @classmethod
    def _create_categ_anglo(cls):
        return cls.env['product.category'].create({
            'name': 'Anglo',
            'parent_id': False,
            'property_cost_method': 'fifo',
            'property_valuation': 'real_time',
            'property_stock_account_input_categ_id': cls.company_data['default_account_stock_in'].id,
            'property_stock_account_output_categ_id': cls.company_data['default_account_stock_out'].id,
        })

    @classmethod
    def _create_taxes(cls):
        """ Create taxes

        tax7: 7%, excluded in product price
        tax10: 10%, included in product price
        """
        tax7 = cls.env['account.tax'].create({'name': 'Tax 7%', 'amount': 7})
        tax10 = cls.env['account.tax'].create({'name': 'Tax 10%', 'amount': 10, 'price_include': True, 'include_base_amount': False})
        (tax7 | tax10).mapped('invoice_repartition_line_ids').write({'account_id': cls.tax_received_account.id})
        (tax7 | tax10).mapped('refund_repartition_line_ids').write({'account_id': cls.tax_received_account.id})

        tax_group_7_10 = tax7.copy()
        with Form(tax_group_7_10) as tax:
            tax.name = 'Tax 7+10%'
            tax.amount_type = 'group'
            tax.children_tax_ids.add(tax7)
            tax.children_tax_ids.add(tax10)

        return {
            'tax7': tax7,
            'tax10': tax10,
            'tax_group_7_10': tax_group_7_10
        }

    ####################
    ## public methods ##
    ####################

    def create_random_uid(self):
        return ('%05d-%03d-%04d' % (randint(1, 99999), randint(1, 999), randint(1, 9999)))

    def create_ui_order_data(self, product_quantity_pairs, customer=False, is_invoiced=False, payments=None, uid=None):
        """ Mocks the order_data generated by the pos ui.

        This is useful in making orders in an open pos session without making tours.
        Its functionality is tested in test_pos_create_ui_order_data.py.

        Before use, make sure that self is set with:
            1. pricelist -> the pricelist of the current session
            2. currency -> currency of the current session
            3. pos_session -> the current session, equivalent to config.current_session_id
            4. cash_pm -> first cash payment method in the current session
            5. config -> the active pos.config

        The above values should be set when `self.open_new_session` is called.

        :param list(tuple) product_quantity_pairs: pair of `ordered product` and `quantity`
        :param list(tuple) payments: pair of `payment_method` and `amount`
        """
        default_fiscal_position = self.config.default_fiscal_position_id
        fiscal_position = customer.property_account_position_id if customer else default_fiscal_position

        def create_order_line(product, quantity):
            price_unit = self.pricelist.get_product_price(product, quantity, False)
            tax_ids = fiscal_position.map_tax(product.taxes_id)
            tax_values = (
                tax_ids.compute_all(price_unit, self.currency, quantity)
                if tax_ids
                else {
                    'total_excluded': price_unit * quantity,
                    'total_included': price_unit * quantity,
                }
            )
            return (0, 0, {
                'discount': 0,
                'id': randint(1, 1000000),
                'pack_lot_ids': [],
                'price_unit': price_unit,
                'product_id': product.id,
                'price_subtotal': tax_values['total_excluded'],
                'price_subtotal_incl': tax_values['total_included'],
                'qty': quantity,
                'tax_ids': [(6, 0, tax_ids.ids)]
            })

        def create_payment(payment_method, amount):
            return (0, 0, {
                'amount': amount,
                'name': fields.Datetime.now(),
                'payment_method_id': payment_method.id,
            })

        uid = uid or self.create_random_uid()

        # 1. generate the order lines
        order_lines = [create_order_line(product, quantity) for product, quantity in product_quantity_pairs]

        # 2. generate the payments
        total_amount_incl = sum(line[2]['price_subtotal_incl'] for line in order_lines)
        if payments is None:
            payments = [create_payment(self.cash_pm, total_amount_incl)]
        else:
            payments = [
                create_payment(pm, amount)
                for pm, amount in payments
            ]

        # 3. complete the fields of the order_data
        total_amount_base = sum(line[2]['price_subtotal'] for line in order_lines)
        return {
            'data': {
                'amount_paid': sum(payment[2]['amount'] for payment in payments),
                'amount_return': 0,
                'amount_tax': total_amount_incl - total_amount_base,
                'amount_total': total_amount_incl,
                'creation_date': fields.Datetime.to_string(fields.Datetime.now()),
                'fiscal_position_id': fiscal_position.id,
                'pricelist_id': self.config.pricelist_id.id,
                'lines': order_lines,
                'name': 'Order %s' % uid,
                'partner_id': customer and customer.id,
                'pos_session_id': self.pos_session.id,
                'sequence_number': 2,
                'statement_ids': payments,
                'uid': uid,
                'user_id': self.env.user.id,
                'to_invoice': is_invoiced,
            },
            'id': uid,
            'to_invoice': is_invoiced,
        }

    @classmethod
    def create_product(cls, name, category, lst_price, standard_price=None, tax_ids=None, sale_account=None):
        product = cls.env['product.product'].create({
            'type': 'product',
            'available_in_pos': True,
            'taxes_id': [(5, 0, 0)] if not tax_ids else [(6, 0, tax_ids)],
            'name': name,
            'categ_id': category.id,
            'lst_price': lst_price,
            'standard_price': standard_price if standard_price else 0.0,
        })
        if sale_account:
            product.property_account_income_id = sale_account
        return product

    @classmethod
    def adjust_inventory(cls, products, quantities):
        """ Adjust inventory of the given products
        """
        inventory = cls.env['stock.inventory'].create({
            'name': 'Inventory adjustment'
        })
        for product, qty in zip(products, quantities):
            cls.env['stock.inventory.line'].create({
                'product_id': product.id,
                'product_uom_id': cls.env.ref('uom.product_uom_unit').id,
                'inventory_id': inventory.id,
                'product_qty': qty,
                'location_id': cls.stock_location_components.id,
            })
        inventory._action_start()
        inventory.action_validate()

    def open_new_session(self):
        """ Used to open new pos session in each configuration.

        - The idea is to properly set values that are constant
          and commonly used in an open pos session.
        - Calling this method is also a prerequisite for using
          `self.create_ui_order_data` function.

        Fields:
            * config : the pos.config currently being used.
                Its value is set at `self.setUp` of the inheriting
                test class.
            * session : the current_session_id of config
            * currency : currency of the current pos.session
            * pricelist : the default pricelist of the session
            * cash_pm : cash payment method of the session
            * bank_pm : bank payment method of the session
            * cash_split_pm : credit payment method of the session
            * bank_split_pm : split bank payment method of the session
        """
        self.config.open_session_cb(check_coa=False)
        self.pos_session = self.config.current_session_id
        self.currency = self.pos_session.currency_id
        self.pricelist = self.pos_session.config_id.pricelist_id
        self.cash_pm = self.pos_session.payment_method_ids.filtered(lambda pm: pm.is_cash_count and not pm.split_transactions)[:1]
        self.bank_pm = self.pos_session.payment_method_ids.filtered(lambda pm: not pm.is_cash_count and not pm.split_transactions)[:1]
        self.cash_split_pm = self.pos_session.payment_method_ids.filtered(lambda pm: pm.is_cash_count and pm.split_transactions)[:1]
        self.bank_split_pm = self.pos_session.payment_method_ids.filtered(lambda pm: not pm.is_cash_count and pm.split_transactions)[:1]