def product_price_update_before_done(self, forced_qty=None): tmpl_dict = defaultdict(lambda: 0.0) # adapt standard price on incomming moves if the product cost_method is 'average' std_price_update = {} for move in self.filtered(lambda move: move.location_id.usage in ('supplier', 'production') and move. product_id.cost_method == 'average'): product_tot_qty_available = move.product_id.qty_available + tmpl_dict[ move.product_id.id] rounding = move.product_id.uom_id.rounding if float_is_zero(product_tot_qty_available, precision_rounding=rounding): new_std_price = move._get_price_unit() elif float_is_zero(product_tot_qty_available + move.product_qty, precision_rounding=rounding): new_std_price = move._get_price_unit() else: # Get the standard price amount_unit = std_price_update.get( (move.company_id.id, move.product_id.id)) or move.product_id.standard_price qty = forced_qty or move.product_qty new_std_price = ( (amount_unit * product_tot_qty_available) + (move._get_price_unit() * qty)) / ( product_tot_qty_available + move.product_qty) tmpl_dict[move.product_id.id] += move.product_qty # Write the standard price, as SUPERUSER_ID because a warehouse manager may not have the right to write on products move.product_id.with_context( force_company=move.company_id.id).sudo().write( {'standard_price': new_std_price}) std_price_update[move.company_id.id, move.product_id.id] = new_std_price
def test_rounding_invalid(self): """ verify that invalid parameters are forbidden """ with self.assertRaises(AssertionError): float_is_zero(0.01, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_compare(0.01, 0.02, precision_digits=3, precision_rounding=0.01) with self.assertRaises(AssertionError): float_round(0.01, precision_digits=3, precision_rounding=0.01)
def _website_price(self): qty = self._context.get('quantity', 1.0) partner = self.env.user.partner_id current_website = self.env['website'].get_current_website() pricelist = current_website.get_current_pricelist() company_id = current_website.company_id context = dict(self._context, pricelist=pricelist.id, partner=partner) self2 = self.with_context( context) if self._context != context else self ret = self.env.user.has_group( 'sale.group_show_price_subtotal' ) and 'total_excluded' or 'total_included' for p, p2 in pycompat.izip(self, self2): taxes = partner.property_account_position_id.map_tax( p.sudo().taxes_id.filtered( lambda x: x.company_id == company_id)) p.website_price = taxes.compute_all(p2.price, pricelist.currency_id, quantity=qty, product=p2, partner=partner)[ret] price_without_pricelist = taxes.compute_all( p.list_price, pricelist.currency_id)[ret] p.website_price_difference = False if float_is_zero( price_without_pricelist - p.website_price, precision_rounding=pricelist.currency_id.rounding) else True p.website_public_price = taxes.compute_all(p2.lst_price, quantity=qty, product=p2, partner=partner)[ret]
def create(self, values): line = super(SaleOrderLine, self).create(values) precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') # check ordered quantity to avoid create project/task when expensing service products if line.state == 'sale' and not float_is_zero( line.product_uom_qty, precision_digits=precision): line._timesheet_service_generation() return line
def is_zero(self, amount): """Returns true if ``amount`` is small enough to be treated as zero according to current currency's rounding rules. Warning: ``is_zero(amount1-amount2)`` is not always equivalent to ``compare_amounts(amount1,amount2) == 0``, as the former will round after computing the difference, while the latter will round before, giving different results for e.g. 0.006 and 0.002 at 2 digits precision. :param float amount: amount to compare with currency's zero With the new API, call it like: ``currency.is_zero(amount)``. """ return tools.float_is_zero(amount, precision_rounding=self.rounding)
def do_change_standard_price(self, new_price, account_id): """ Changes the Standard Price of Product and creates an account move accordingly.""" AccountMove = self.env['account.move'] quant_locs = self.env['stock.quant'].sudo().read_group([('product_id', 'in', self.ids)], ['location_id'], ['location_id']) quant_loc_ids = [loc['location_id'][0] for loc in quant_locs] locations = self.env['stock.location'].search([('usage', '=', 'internal'), ('company_id', '=', self.env.user.company_id.id), ('id', 'in', quant_loc_ids)]) product_accounts = {product.id: product.product_tmpl_id.get_product_accounts() for product in self} for location in locations: for product in self.with_context(location=location.id, compute_child=False).filtered(lambda r: r.valuation == 'real_time'): diff = product.standard_price - new_price if float_is_zero(diff, precision_rounding=product.currency_id.rounding): raise UserError(_("No difference between standard price and new price!")) if not product_accounts[product.id].get('stock_valuation', False): raise UserError(_('You don\'t have any stock valuation account defined on your product category. You must define one before processing this operation.')) qty_available = product.qty_available if qty_available: # Accounting Entries if diff * qty_available > 0: debit_account_id = account_id credit_account_id = product_accounts[product.id]['stock_valuation'].id else: debit_account_id = product_accounts[product.id]['stock_valuation'].id credit_account_id = account_id move_vals = { 'journal_id': product_accounts[product.id]['stock_journal'].id, 'company_id': location.company_id.id, 'line_ids': [(0, 0, { 'name': _('Standard Price changed - %s') % (product.display_name), 'account_id': debit_account_id, 'debit': abs(diff * qty_available), 'credit': 0, }), (0, 0, { 'name': _('Standard Price changed - %s') % (product.display_name), 'account_id': credit_account_id, 'debit': 0, 'credit': abs(diff * qty_available), })], } move = AccountMove.create(move_vals) move.post() self.write({'standard_price': new_price}) return True
def summary(self): res = super(EventRegistration, self).summary() if self.event_ticket_id.product_id.image_medium: res['image'] = '/web/image/product.product/%s/image_medium' % self.event_ticket_id.product_id.id information = res.setdefault('information', {}) information.append((_('Name'), self.name)) information.append((_('Ticket'), self.event_ticket_id.name or _('None'))) order = self.sale_order_id.sudo() order_line = self.sale_order_line_id.sudo() if not order or float_is_zero( order_line.price_total, precision_digits=order.currency_id.rounding): payment_status = _('Free') elif not order.invoice_ids or any(invoice.state != 'paid' for invoice in order.invoice_ids): payment_status = _('To pay') res['alert'] = _('The registration must be paid') else: payment_status = _('Paid') information.append((_('Payment'), payment_status)) return res
def action_sheet_move_create(self): if any(sheet.state != 'approve' for sheet in self): raise UserError( _("You can only generate accounting entry for approved expense(s)." )) if any(not sheet.journal_id for sheet in self): raise UserError( _("Expenses must have an expense journal specified to generate accounting entries." )) expense_line_ids = self.mapped('expense_line_ids')\ .filtered(lambda r: not float_is_zero(r.total_amount, precision_rounding=(r.currency_id or self.env.user.company_id.currency_id).rounding)) res = expense_line_ids.action_move_create() if not self.accounting_date: self.accounting_date = self.account_move_id.date if self.payment_mode == 'own_account' and expense_line_ids: self.write({'state': 'post'}) else: self.write({'state': 'done'}) return res
def compute_depreciation_board(self): self.ensure_one() posted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: x.move_check).sorted(key=lambda l: l.depreciation_date) unposted_depreciation_line_ids = self.depreciation_line_ids.filtered(lambda x: not x.move_check) # Remove old unposted depreciation lines. We cannot use unlink() with One2many field commands = [(2, line_id.id, False) for line_id in unposted_depreciation_line_ids] if self.value_residual != 0.0: amount_to_depr = residual_amount = self.value_residual if self.prorata: # if we already have some previous validated entries, starting date is last entry + method perio if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[-1].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: depreciation_date = datetime.strptime(self._get_last_depreciation_date()[self.id], DF).date() else: # depreciation_date = 1st of January of purchase year if annual valuation, 1st of # purchase month in other cases if self.method_period >= 12: asset_date = datetime.strptime(self.date[:4] + '-01-01', DF).date() else: asset_date = datetime.strptime(self.date[:7] + '-01', DF).date() # if we already have some previous validated entries, starting date isn't 1st January but last entry + method period if posted_depreciation_line_ids and posted_depreciation_line_ids[-1].depreciation_date: last_depreciation_date = datetime.strptime(posted_depreciation_line_ids[-1].depreciation_date, DF).date() depreciation_date = last_depreciation_date + relativedelta(months=+self.method_period) else: depreciation_date = asset_date day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year total_days = (year % 4) and 365 or 366 undone_dotation_number = self._compute_board_undone_dotation_nb(depreciation_date, total_days) for x in range(len(posted_depreciation_line_ids), undone_dotation_number): sequence = x + 1 amount = self._compute_board_amount(sequence, residual_amount, amount_to_depr, undone_dotation_number, posted_depreciation_line_ids, total_days, depreciation_date) amount = self.currency_id.round(amount) if float_is_zero(amount, precision_rounding=self.currency_id.rounding): continue residual_amount -= amount vals = { 'amount': amount, 'asset_id': self.id, 'sequence': sequence, 'name': (self.code or '') + '/' + str(sequence), 'remaining_value': residual_amount, 'depreciated_value': self.value - (self.salvage_value + residual_amount), 'depreciation_date': depreciation_date.strftime(DF), } commands.append((0, False, vals)) # Considering Depr. Period as months depreciation_date = date(year, month, day) + relativedelta(months=+self.method_period) day = depreciation_date.day month = depreciation_date.month year = depreciation_date.year self.write({'depreciation_line_ids': commands}) return True
def action_payslip_done(self): precision = self.env['decimal.precision'].precision_get('Payroll') for slip in self: line_ids = [] debit_sum = 0.0 credit_sum = 0.0 date = slip.date or slip.date_to name = _('Payslip of %s') % (slip.employee_id.name) move_dict = { 'narration': name, 'ref': slip.number, 'journal_id': slip.journal_id.id, 'date': date, } for line in slip.details_by_salary_rule_category: amount = slip.credit_note and -line.total or line.total if float_is_zero(amount, precision_digits=precision): continue debit_account_id = line.salary_rule_id.account_debit.id credit_account_id = line.salary_rule_id.account_credit.id if debit_account_id: debit_line = (0, 0, { 'name': line.name, 'partner_id': line._get_partner_id(credit_account=False), 'account_id': debit_account_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': amount > 0.0 and amount or 0.0, 'credit': amount < 0.0 and -amount or 0.0, 'analytic_account_id': line.salary_rule_id.analytic_account_id.id, 'tax_line_id': line.salary_rule_id.account_tax_id.id, }) line_ids.append(debit_line) debit_sum += debit_line[2]['debit'] - debit_line[2]['credit'] if credit_account_id: credit_line = (0, 0, { 'name': line.name, 'partner_id': line._get_partner_id(credit_account=True), 'account_id': credit_account_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': amount < 0.0 and -amount or 0.0, 'credit': amount > 0.0 and amount or 0.0, 'analytic_account_id': line.salary_rule_id.analytic_account_id.id, 'tax_line_id': line.salary_rule_id.account_tax_id.id, }) line_ids.append(credit_line) credit_sum += credit_line[2]['credit'] - credit_line[2]['debit'] if float_compare(credit_sum, debit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_credit_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Credit Account!') % (slip.journal_id.name)) adjust_credit = (0, 0, { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': 0.0, 'credit': debit_sum - credit_sum, }) line_ids.append(adjust_credit) elif float_compare(debit_sum, credit_sum, precision_digits=precision) == -1: acc_id = slip.journal_id.default_debit_account_id.id if not acc_id: raise UserError(_('The Expense Journal "%s" has not properly configured the Debit Account!') % (slip.journal_id.name)) adjust_debit = (0, 0, { 'name': _('Adjustment Entry'), 'partner_id': False, 'account_id': acc_id, 'journal_id': slip.journal_id.id, 'date': date, 'debit': credit_sum - debit_sum, 'credit': 0.0, }) line_ids.append(adjust_debit) move_dict['line_ids'] = line_ids move = self.env['account.move'].create(move_dict) slip.write({'move_id': move.id, 'date': date}) move.post() return super(HrPayslip, self).action_payslip_done()
def change_prod_qty(self): precision = self.env['decimal.precision'].precision_get( 'Product Unit of Measure') for wizard in self: production = wizard.mo_id produced = sum( production.move_finished_ids.filtered( lambda m: m.product_id == production.product_id).mapped( 'quantity_done')) if wizard.product_qty < produced: raise UserError( _("You have already processed %d. Please input a quantity higher than %d " ) % (produced, produced)) production.write({'product_qty': wizard.product_qty}) done_moves = production.move_finished_ids.filtered( lambda x: x.state == 'done' and x.product_id == production. product_id) qty_produced = production.product_id.uom_id._compute_quantity( sum(done_moves.mapped('product_qty')), production.product_uom_id) factor = production.product_uom_id._compute_quantity( production.product_qty - qty_produced, production.bom_id. product_uom_id) / production.bom_id.product_qty boms, lines = production.bom_id.explode( production.product_id, factor, picking_type=production.bom_id.picking_type_id) for line, line_data in lines: production._update_raw_move(line, line_data) operation_bom_qty = {} for bom, bom_data in boms: for operation in bom.routing_id.operation_ids: operation_bom_qty[operation.id] = bom_data['qty'] self._update_product_to_produce( production, production.product_qty - qty_produced) moves = production.move_raw_ids.filtered(lambda x: x.state not in ('done', 'cancel')) moves._action_assign() for wo in production.workorder_ids: operation = wo.operation_id if operation_bom_qty.get(operation.id): cycle_number = math.ceil(operation_bom_qty[operation.id] / operation.workcenter_id.capacity ) # TODO: float_round UP wo.duration_expected = ( operation.workcenter_id.time_start + operation.workcenter_id.time_stop + cycle_number * operation.time_cycle * 100.0 / operation.workcenter_id.time_efficiency) quantity = wo.qty_production - wo.qty_produced if production.product_id.tracking == 'serial': quantity = 1.0 if not float_is_zero( quantity, precision_digits=precision) else 0.0 else: quantity = quantity if (quantity > 0) else 0 if float_is_zero(quantity, precision_digits=precision): wo.final_lot_id = False wo.active_move_line_ids.unlink() wo.qty_producing = quantity if wo.qty_produced < wo.qty_production and wo.state == 'done': wo.state = 'progress' # assign moves; last operation receive all unassigned moves # TODO: following could be put in a function as it is similar as code in _workorders_create # TODO: only needed when creating new moves moves_raw = production.move_raw_ids.filtered( lambda move: move.operation_id == operation and move.state not in ('done', 'cancel')) if wo == production.workorder_ids[-1]: moves_raw |= production.move_raw_ids.filtered( lambda move: not move.operation_id) moves_finished = production.move_finished_ids.filtered( lambda move: move.operation_id == operation ) #TODO: code does nothing, unless maybe by_products? moves_raw.mapped('move_line_ids').write( {'workorder_id': wo.id}) (moves_finished + moves_raw).write({'workorder_id': wo.id}) if quantity > 0 and wo.move_raw_ids.filtered( lambda x: x.product_id.tracking != 'none' ) and not wo.active_move_line_ids: wo._generate_lot_ids() return {}
def test_timesheet_manual(self): """ Test timesheet invoicing with 'invoice on delivery' timetracked products """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_usd.id, 'partner_invoice_id': self.partner_usd.id, 'partner_shipping_id': self.partner_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_manual_global_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_manual2.name, 'product_id': self.product_delivery_manual2.id, 'product_uom_qty': 50, 'product_uom': self.product_delivery_manual2.uom_id.id, 'price_unit': self.product_delivery_manual2.list_price, 'order_id': sale_order.id, }) so_line_manual_only_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_manual4.name, 'product_id': self.product_delivery_manual4.id, 'product_uom_qty': 20, 'product_uom': self.product_delivery_manual4.uom_id.id, 'price_unit': self.product_delivery_manual4.list_price, 'order_id': sale_order.id, }) # confirm SO sale_order.action_confirm() self.assertTrue(sale_order.project_project_id, "Sales Order should have create a project") self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: manually product should not need to be invoiced on so confirmation' ) # let's log some timesheets (on task and project) timesheet1 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': self.project_global.id, # global project 'task_id': so_line_manual_global_project.task_id.id, 'unit_amount': 6, 'employee_id': self.employee_manager.id, }) timesheet2 = self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, # global project 'unit_amount': 3, 'employee_id': self.employee_manager.id, }) self.assertEqual( so_line_manual_global_project.task_id.sale_line_id, so_line_manual_global_project, "Task from a milestone product should be linked to its SO line too" ) self.assertEqual( timesheet1.timesheet_invoice_type, 'billable_fixed', "Milestone timesheet goes in billable fixed category") self.assertTrue( float_is_zero(so_line_manual_global_project.qty_delivered, precision_digits=2), "Milestone Timesheeting should not incremented the delivered quantity on the SO line" ) self.assertEqual( so_line_manual_global_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created task." ) self.assertEqual( so_line_manual_only_project.qty_to_invoice, 0.0, "Manual service should not be affected by timesheet on their created project." ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation' )
def test_timesheet_delivery(self): """ Test timesheet invoicing with 'invoice on delivery' timetracked products 1. Create SO and confirm it 2. log timesheet 3. create invoice 4. log other timesheet 5. create a second invoice 6. add new SO line (delivered service) """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_usd.id, 'partner_invoice_id': self.partner_usd.id, 'partner_shipping_id': self.partner_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_deliver_global_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet2.name, 'product_id': self.product_delivery_timesheet2.id, 'product_uom_qty': 50, 'product_uom': self.product_delivery_timesheet2.uom_id.id, 'price_unit': self.product_delivery_timesheet2.list_price, 'order_id': sale_order.id, }) so_line_deliver_task_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet3.name, 'product_id': self.product_delivery_timesheet3.id, 'product_uom_qty': 20, 'product_uom': self.product_delivery_timesheet3.uom_id.id, 'price_unit': self.product_delivery_timesheet3.list_price, 'order_id': sale_order.id, }) so_line_deliver_global_project.product_id_change() so_line_deliver_task_project.product_id_change() # confirm SO sale_order.action_confirm() task_serv2 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_deliver_global_project.id) ]) task_serv3 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_deliver_task_project.id) ]) self.assertEqual( task_serv2.project_id, self.project_global, "Sale Timesheet: task should be created in global project") self.assertTrue( task_serv2, "Sale Timesheet: on SO confirmation, a task should have been created in global project" ) self.assertTrue( task_serv3, "Sale Timesheet: on SO confirmation, a task should have been created in a new project" ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" should not need to be invoiced on so confirmation' ) self.assertEqual(sale_order.analytic_account_id, task_serv3.project_id.analytic_account_id, "SO should have create a project") self.assertEqual( sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation" ) # let's log some timesheets self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv2.project_id.id, # global project 'task_id': task_serv2.id, 'unit_amount': 10.5, 'employee_id': self.employee_manager.id, }) self.assertEqual( so_line_deliver_global_project.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged' ) self.assertEqual( so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line' ) self.assertEqual( sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so in "to invoice" status when logged' ) # invoice SO invoice_id = sale_order.action_invoice_create() invoice = self.env['account.invoice'].browse(invoice_id) self.assertTrue( float_is_zero(invoice.amount_total - so_line_deliver_global_project.price_unit * 10.5, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') # log some timesheets again self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': task_serv2.project_id.id, # global project 'task_id': task_serv2.id, 'unit_amount': 39.5, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_deliver_global_project.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged' ) self.assertEqual( so_line_deliver_task_project.invoice_status, 'no', 'Sale Timesheet: so line invoice status should not change when no timesheet linked to the line' ) self.assertEqual( sale_order.invoice_status, 'to invoice', 'Sale Timesheet: "invoice on delivery" timesheets should not modify the invoice_status of the so' ) # create a second invoice sale_order.action_invoice_create() self.assertEqual( so_line_deliver_global_project.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on delivery" timesheets should set the so line in "to invoice" status when logged' ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now' ) # add a line on SO so_line_deliver_only_project = self.env['sale.order.line'].create({ 'name': self.product_delivery_timesheet4.name, 'product_id': self.product_delivery_timesheet4.id, 'product_uom_qty': 5, 'product_uom': self.product_delivery_timesheet4.uom_id.id, 'price_unit': self.product_delivery_timesheet4.list_price, 'order_id': sale_order.id, }) self.assertEqual( sale_order.project_project_id, task_serv3.project_id, "SO should not have create a second project, since so line 3 already create one project for the SO" ) # let's log some timesheets on the project self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, # global project 'unit_amount': 7, 'employee_id': self.employee_user.id, }) self.assertTrue( float_is_zero(so_line_deliver_only_project.qty_delivered, precision_digits=2), "Timesheeting on project should not incremented the delivered quantity on the SO line" ) self.assertEqual( sale_order.invoice_status, 'no', 'Sale Timesheet: "invoice on delivery" timesheets should be invoiced completely by now' )
def test_timesheet_order(self): """ Test timesheet invoicing with 'invoice on order' timetracked products 1. create SO with 2 ordered product and confirm 2. create invoice 3. log timesheet 4. add new SO line (ordered service) 5. create new invoice """ # create SO and confirm it sale_order = self.env['sale.order'].create({ 'partner_id': self.partner_usd.id, 'partner_invoice_id': self.partner_usd.id, 'partner_shipping_id': self.partner_usd.id, 'pricelist_id': self.pricelist_usd.id, }) so_line_ordered_project_only = self.env['sale.order.line'].create({ 'name': self.product_order_timesheet4.name, 'product_id': self.product_order_timesheet4.id, 'product_uom_qty': 10, 'product_uom': self.product_order_timesheet4.uom_id.id, 'price_unit': self.product_order_timesheet4.list_price, 'order_id': sale_order.id, }) so_line_ordered_global_project = self.env['sale.order.line'].create({ 'name': self.product_order_timesheet2.name, 'product_id': self.product_order_timesheet2.id, 'product_uom_qty': 50, 'product_uom': self.product_order_timesheet2.uom_id.id, 'price_unit': self.product_order_timesheet2.list_price, 'order_id': sale_order.id, }) so_line_ordered_project_only.product_id_change() so_line_ordered_global_project.product_id_change() sale_order.action_confirm() task_serv2 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_ordered_global_project.id) ]) self.assertEqual( sale_order.tasks_count, 1, "One task should have been created on SO confirmation") self.assertTrue( sale_order.project_project_id, "A project should have been created by the SO, when confirmed.") # create invoice sale_order.action_invoice_create() # let's log some timesheets (on the project created by so_line_ordered_project_only) self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, 'task_id': task_serv2.id, 'unit_amount': 10.5, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_ordered_global_project.qty_delivered, 10.5, 'Timesheet directly on project does not increase delivered quantity on so line' ) self.assertEqual( sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so' ) self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, 'task_id': task_serv2.id, 'unit_amount': 39.5, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_ordered_global_project.qty_delivered, 50, 'Sale Timesheet: timesheet does not increase delivered quantity on so line' ) self.assertEqual( sale_order.invoice_status, 'invoiced', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so' ) self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, 'unit_amount': 10, 'employee_id': self.employee_user.id, }) self.assertEqual( so_line_ordered_project_only.qty_delivered, 0.0, 'Timesheet directly on project does not increase delivered quantity on so line' ) # log timesheet on task in global project (higher than the initial ordrered qty) self.env['account.analytic.line'].create({ 'name': 'Test Line', 'project_id': sale_order.project_project_id.id, 'task_id': task_serv2.id, 'unit_amount': 5, 'employee_id': self.employee_user.id, }) self.assertEqual( sale_order.invoice_status, 'upselling', 'Sale Timesheet: "invoice on order" timesheets should not modify the invoice_status of the so' ) # add so line with produdct "create task in new project". (project will be the one from SO) so_line_ordered_task_new_project = self.env['sale.order.line'].create({ 'name': self.product_order_timesheet3.name, 'product_id': self.product_order_timesheet3.id, 'product_uom_qty': 3, 'product_uom': self.product_order_timesheet3.uom_id.id, 'price_unit': self.product_order_timesheet3.list_price, 'order_id': sale_order.id, }) task_serv3 = self.env['project.task'].search([ ('sale_line_id', '=', so_line_ordered_task_new_project.id) ]) self.assertEqual( sale_order.invoice_status, 'to invoice', 'Sale Timesheet: Adding a new service line (so line) should put the SO in "to invocie" state.' ) self.assertEqual( sale_order.tasks_count, 2, "Two tasks (1 per SO line) should have been created on SO confirmation" ) # create invoice invoice_id = sale_order.action_invoice_create() invoice = self.env['account.invoice'].browse(invoice_id) self.assertEqual( len(sale_order.invoice_ids), 2, "A second invoice should have been created from the SO") self.assertTrue( float_is_zero(invoice.amount_total - so_line_ordered_task_new_project.price_unit * 3, precision_digits=2), 'Sale: invoice generation on timesheets product is wrong') self.assertEqual( sale_order.project_project_id, task_serv3.project_id, "When creating task in new project, the task should be in SO project (if already exists), otherwise it created one." )
def try_zero(amount, expected): self.assertEqual(float_is_zero(amount, precision_digits=3), expected, "Rounding error: %s should be zero!" % amount)
def _get_partner_move_lines(self, account_type, date_from, target_move, period_length, branch_id): periods = {} start = datetime.strptime(date_from, "%Y-%m-%d") for i in range(5)[::-1]: stop = start - relativedelta(days=period_length) periods[str(i)] = { 'name': (i != 0 and (str( (5 - (i + 1)) * period_length) + '-' + str( (5 - i) * period_length)) or ('+' + str(4 * period_length))), 'stop': start.strftime('%Y-%m-%d'), 'start': (i != 0 and stop.strftime('%Y-%m-%d') or False), } start = stop - relativedelta(days=1) res = [] total = [] cr = self.env.cr company_ids = self.env.context.get('company_ids', (self.env.user.company_id.id, )) move_state = ['draft', 'posted'] branch = '' if branch_id: branch = 'AND (l.branch_id =' + str(branch_id[0]) + ')' if target_move == 'posted': move_state = ['posted'] arg_list = (tuple(move_state), tuple(account_type)) #build the reconciliation clause to see what partner needs to be printed reconciliation_clause = '(l.reconciled IS FALSE)' cr.execute( 'SELECT debit_move_id, credit_move_id FROM account_partial_reconcile where create_date > %s', (date_from, )) reconciled_after_date = [] for row in cr.fetchall(): reconciled_after_date += [row[0], row[1]] if reconciled_after_date: reconciliation_clause = '(l.reconciled IS FALSE OR l.id IN %s)' arg_list += (tuple(reconciled_after_date), ) arg_list += (date_from, tuple(company_ids)) query = ''' SELECT DISTINCT l.partner_id, UPPER(res_partner.name) FROM account_move_line AS l left join res_partner on l.partner_id = res_partner.id, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND ''' + reconciliation_clause + branch + ''' AND (l.date <= %s) AND l.company_id IN %s ORDER BY UPPER(res_partner.name)''' cr.execute(query, arg_list) partners = cr.dictfetchall() # put a total of 0 for i in range(7): total.append(0) # Build a string like (1,2,3) for easy use in SQL query partner_ids = [ partner['partner_id'] for partner in partners if partner['partner_id'] ] lines = dict( (partner['partner_id'] or False, []) for partner in partners) if not partner_ids: return [], [], [] # This dictionary will store the not due amount of all partners undue_amounts = {} query = '''SELECT l.id FROM account_move_line AS l, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND (COALESCE(l.date_maturity,l.date) > %s)\ AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) AND (l.date <= %s) ''' + branch + ''' AND l.company_id IN %s''' cr.execute(query, (tuple(move_state), tuple(account_type), date_from, tuple(partner_ids), date_from, tuple(company_ids))) aml_ids = cr.fetchall() aml_ids = aml_ids and [x[0] for x in aml_ids] or [] for line in self.env['account.move.line'].browse(aml_ids): partner_id = line.partner_id.id or False if partner_id not in undue_amounts: undue_amounts[partner_id] = 0.0 line_amount = line.balance if line.balance == 0: continue for partial_line in line.matched_debit_ids: if partial_line.max_date <= date_from: line_amount += partial_line.amount for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: line_amount -= partial_line.amount if not self.env.user.company_id.currency_id.is_zero(line_amount): undue_amounts[partner_id] += line_amount lines[partner_id].append({ 'line': line, 'amount': line_amount, 'period': 6, }) # Use one query per period and store results in history (a list variable) # Each history will contain: history[1] = {'<partner_id>': <partner_debit-credit>} history = [] for i in range(5): args_list = ( tuple(move_state), tuple(account_type), tuple(partner_ids), ) dates_query = '(COALESCE(l.date_maturity,l.date)' if periods[str(i)]['start'] and periods[str(i)]['stop']: dates_query += ' BETWEEN %s AND %s)' args_list += (periods[str(i)]['start'], periods[str(i)]['stop']) elif periods[str(i)]['start']: dates_query += ' >= %s)' args_list += (periods[str(i)]['start'], ) else: dates_query += ' <= %s)' args_list += (periods[str(i)]['stop'], ) args_list += (date_from, tuple(company_ids)) query = '''SELECT l.id FROM account_move_line AS l, account_account, account_move am WHERE (l.account_id = account_account.id) AND (l.move_id = am.id) AND (am.state IN %s) AND (account_account.internal_type IN %s) AND ((l.partner_id IN %s) OR (l.partner_id IS NULL)) AND ''' + dates_query + branch + ''' AND (l.date <= %s) AND l.company_id IN %s''' cr.execute(query, args_list) partners_amount = {} aml_ids = cr.fetchall() aml_ids = aml_ids and [x[0] for x in aml_ids] or [] for line in self.env['account.move.line'].browse(aml_ids): partner_id = line.partner_id.id or False if partner_id not in partners_amount: partners_amount[partner_id] = 0.0 line_amount = line.balance if line.balance == 0: continue for partial_line in line.matched_debit_ids: if partial_line.max_date <= date_from: line_amount += partial_line.amount for partial_line in line.matched_credit_ids: if partial_line.max_date <= date_from: line_amount -= partial_line.amount if not self.env.user.company_id.currency_id.is_zero( line_amount): partners_amount[partner_id] += line_amount lines[partner_id].append({ 'line': line, 'amount': line_amount, 'period': i + 1, }) history.append(partners_amount) for partner in partners: if partner['partner_id'] is None: partner['partner_id'] = False at_least_one_amount = False values = {} undue_amt = 0.0 if partner[ 'partner_id'] in undue_amounts: # Making sure this partner actually was found by the query undue_amt = undue_amounts[partner['partner_id']] total[6] = total[6] + undue_amt values['direction'] = undue_amt if not float_is_zero(values['direction'], precision_rounding=self.env.user.company_id. currency_id.rounding): at_least_one_amount = True for i in range(5): during = False if partner['partner_id'] in history[i]: during = [history[i][partner['partner_id']]] # Adding counter total[(i)] = total[(i)] + (during and during[0] or 0) values[str(i)] = during and during[0] or 0.0 if not float_is_zero(values[str(i)], precision_rounding=self.env.user. company_id.currency_id.rounding): at_least_one_amount = True values['total'] = sum([values['direction']] + [values[str(i)] for i in range(5)]) ## Add for total total[(i + 1)] += values['total'] values['partner_id'] = partner['partner_id'] if partner['partner_id']: browsed_partner = self.env['res.partner'].browse( partner['partner_id']) values['name'] = browsed_partner.name and len( browsed_partner.name) >= 45 and browsed_partner.name[ 0:40] + '...' or browsed_partner.name values['trust'] = browsed_partner.trust else: values['name'] = _('Unknown Partner') values['trust'] = False if at_least_one_amount or self._context.get( 'include_nullified_amount'): res.append(values) return res, total, lines