Example #1
0
class AttributeOption(Model):
    _name = "product.attribute.option"
    _fields = {
        "attribute_id":
        fields.Many2One("product.attribute", "Attribute", required=True),
        "name":
        fields.Char("Name", required=True, translate=True, size=256),
        "code":
        fields.Char("Code", required=True, size=256),
        "sequence":
        fields.Integer("Sequence", required=True),
        "description":
        fields.Text("Description"),
        "image":
        fields.File("Image"),
        "price":
        fields.Float("Price"),
    }
    _order = "sequence"
    _defaults = {
        "sequence": 0,
    }
Example #2
0
class Move(Model):
    _name = "account.move"
    _string = "Journal Entry"
    _name_field = "number"
    _multi_company = True
    _audit_log = True
    _key = ["company_id", "number"]
    _fields = {
        "journal_id":
        fields.Many2One("account.journal",
                        "Journal",
                        required=True,
                        search=True),
        "narration":
        fields.Text("Narration", required=True, search=True),
        "date":
        fields.Date("Document Date", required=True, search=True, index=True),
        "date_posted":
        fields.Date("Posted Date", search=True, index=True),
        "state":
        fields.Selection(
            [["draft", "Draft"], ["posted", "Posted"], ["voided", "Voided"]],
            "Status",
            required=True,
            search=True),
        "lines":
        fields.One2Many("account.move.line", "move_id", "Lines"),
        "total_debit":
        fields.Decimal("Total Debit",
                       function="get_total",
                       function_multi=True),
        "total_credit":
        fields.Decimal("Total Credit",
                       function="get_total",
                       function_multi=True),
        "type":
        fields.Selection([["auto", "auto"], ["manual", "Manual"]], "Type"),
        "ref":
        fields.Char("Reference", search=True),
        "number":
        fields.Char("Number", search=True, required=True),
        "default_line_desc":
        fields.Boolean("Default narration to line description"),
        "comments":
        fields.One2Many("message", "related_id", "Comments"),
        "related_id":
        fields.Reference(
            [["account.invoice", "Invoice"], ["account.payment", "Payment"],
             ["account.transfer", "Transfer"], ["hr.expense", "Expense Claim"],
             ["service.contract", "Service Contract"], ["pawn.loan", "Loan"],
             ["landed.cost", "Landed Cost"],
             ["stock.picking", "Stock Picking"]], "Related To"),
        "company_id":
        fields.Many2One("company", "Company"),
        "track_entries":
        fields.One2Many("account.track.entry", "move_id", "Tracking Entries"),
        "difference":
        fields.Float("Difference",
                     function="get_difference",
                     function_multi=True),
    }

    def _get_journal(self, context={}):
        settings = get_model("settings").browse(1)
        return settings.general_journal_id.id

    def _get_number(self, context={}):
        journal_id = context.get("journal_id")
        if not journal_id:
            settings = get_model("settings").browse(1)
            journal_id = settings.general_journal_id.id
        if not journal_id:
            return
        journal = get_model("account.journal").browse(journal_id)
        seq_id = journal.sequence_id.id
        if not seq_id:
            return
        while 1:
            num = get_model("sequence").get_next_number(seq_id,
                                                        context=context)
            res = self.search([["number", "=", num]])
            if not res:
                return num
            get_model("sequence").increment_number(seq_id, context=context)

    _defaults = {
        "state": "draft",
        "default_line_desc": True,
        "type": "auto",
        "date": lambda *a: time.strftime("%Y-%m-%d"),
        "journal_id": _get_journal,
        "number": _get_number,
        "company_id": lambda *a: get_active_company(),
    }
    _order = "date desc,id desc"

    def get_difference(self, ids, context):
        vals = {}
        for obj in self.browse(ids):
            vals[obj.id] = {
                "difference": obj.total_debit - obj.total_credit,
            }
        return vals

    def create(self, vals, **kw):
        t0 = time.time()
        new_id = super().create(vals, **kw)
        t01 = time.time()
        dt01 = (t01 - t0) * 1000
        print("account_move.dt01", dt01)
        obj = self.browse([new_id])[0]
        line_ids = []
        rec_ids = []
        for line in obj.lines:
            line_ids.append(line.id)
            if line.reconcile_id:
                rec_ids.append(line.reconcile_id.id)
        get_model("account.move.line").function_store(line_ids)
        if rec_ids:
            get_model("account.reconcile").function_store(rec_ids)
        get_model("field.cache").clear_cache(model="account.account")
        t1 = time.time()
        dt = (t1 - t0) * 1000
        print("account_move.create <<< %d ms" % dt)
        return new_id

    def write(self, ids, vals, **kw):
        super().write(ids, vals, **kw)
        line_ids = []
        rec_ids = []
        for obj in self.browse(ids):
            for line in obj.lines:
                line_ids.append(line.id)
                if line.reconcile_id:
                    rec_ids.append(line.reconcile_id.id)
        get_model("account.move.line").function_store(line_ids)
        if rec_ids:
            get_model("account.reconcile").function_store(rec_ids)
            move_ids = []
            for rec in get_model("account.reconcile").browse(rec_ids):
                for line in rec.lines:
                    move_ids.append(line.move_id.id)
            move_ids = list(set(move_ids))
            inv_ids = get_model("account.invoice").search(
                [["move_id", "in", move_ids]])
            if inv_ids:
                get_model("account.invoice").function_store(
                    inv_ids)  # XXX: check this
        get_model("field.cache").clear_cache(model="account.account")

    def delete(self, ids, **kw):
        rec_ids = []
        for obj in self.browse(ids):
            if obj.state == "posted":
                raise Exception("Can not deleted posted journal entry")
            for line in obj.lines:
                if line.reconcile_id:
                    rec_ids.append(line.reconcile_id.id)
        super().delete(ids, **kw)
        if rec_ids:
            get_model("account.reconcile").function_store(rec_ids)
        get_model("field.cache").clear_cache(model="account.account")

    def post(self, ids, context={}):
        settings = get_model("settings").browse(1)
        for obj in self.browse(ids):
            if settings.lock_date:
                assert obj.date >= settings.lock_date, "Accounting transaction is before lock date"
            if obj.state != "draft":
                raise Exception("Journal entry is not draft")
            total_debit = 0
            total_credit = 0
            for line in obj.lines:
                acc = line.account_id
                if acc.type == "view":
                    raise Exception(
                        "Can not post to 'view' account ([%s] %s)" %
                        (acc.code, acc.name))
                if acc.company_id.id != obj.company_id.id:
                    raise Exception(
                        "Wrong company for account %s in journal entry %s (account company: %s, journal entry company %s)("
                        % (acc.code, obj.number, acc.company_id.code,
                           obj.company_id.code))
                if acc.require_contact and not line.contact_id:
                    raise Exception("Missing contact for account %s" %
                                    acc.code)
                if acc.require_tax_no and not line.tax_no:
                    raise Exception("Missing tax number for account %s" %
                                    acc.code)
                if acc.require_track and not line.track_id:
                    raise Exception(
                        "Missing tracking category for account %s" % acc.code)
                if acc.require_track2 and not line.track2_id:
                    raise Exception(
                        "Missing secondary tracking category for account %s" %
                        acc.code)
                if line.debit < 0:
                    raise Exception("Debit amount is negative (%s)" %
                                    line.debit)
                if line.credit < 0:
                    raise Exception("Credit amount is negative (%s)" %
                                    line.credit)
                if line.debit > 0 and line.credit > 0:
                    raise Exception(
                        "Debit and credit amounts can not be both non-zero (account: %s, debit: %s, credit: %s)"
                        % (line.account_id.name_get()[0][1], line.debit,
                           line.credit))
                total_debit += line.debit
                total_credit += line.credit
                if line.tax_comp_id and not line.tax_date:
                    line.write({"tax_date": line.move_id.date})
                if acc.currency_id.id != settings.currency_id.id and line.amount_cur is None:
                    raise Exception("Missing currency amount for account %s" %
                                    line.account_id.name_get()[0][1])
                if line.amount_cur is not None and acc.currency_id.id == settings.currency_id.id:
                    raise Exception(
                        "Currency amount for account %s should be empty" %
                        line.account_id.name_get()[0][1])
                if line.amount_cur is not None and line.amount_cur < 0:
                    raise Exception("Currency amount is negative (%s)" %
                                    line.amount_cur)
            if abs(total_debit - total_credit) != 0:
                print("NOT BALANCED total_debit=%s total_credit=%s" %
                      (total_debit, total_credit))
                for line in obj.lines:
                    print("  ACC: [%s] %s DR: %s CR: %s" %
                          (line.account_id.code, line.account_id.name,
                           line.debit, line.credit))
                raise Exception(
                    "Journal entry is not balanced (debit=%s, credit=%s)" %
                    (total_debit, total_credit))
            obj.write({"state": "posted"})
            if not obj.date_posted:
                date_posted = time.strftime("%Y-%m-%d")
                obj.write({"date_posted": date_posted})
            obj.create_track_entries()
            seq = 1
            for line in obj.lines:
                line.write({"sequence": seq})  # XXX
                seq += 1
        if not context.get("no_reconcile"):
            bank_ids = []
            for obj in self.browse(ids):
                for line in obj.lines:
                    acc = line.account_id
                    if acc.type in ("bank", "cash", "cheque"):
                        bank_ids.append(acc.id)
            if bank_ids:
                bank_ids = list(set(bank_ids))
                get_model("account.account").auto_bank_reconcile(bank_ids)
        get_model("account.balance").update_balances()

    def create_track_entries(self, ids, context={}):
        obj = self.browse(ids[0])
        settings = get_model("settings").browse(1)
        for line in obj.lines:
            if line.track_id:
                amt = line.credit - line.debit
                if line.track_id.currency_id:
                    amt = get_model("currency").convert(
                        amt, settings.currency_id.id,
                        line.track_id.currency_id.id)
                vals = {
                    "date": obj.date,
                    "track_id": line.track_id.id,
                    "amount": amt,
                    "description": line.description,
                    "move_id": obj.id,
                }
                get_model("account.track.entry").create(vals)
            if line.track2_id:
                amt = line.credit - line.debit
                if line.track2_id.currency_id:
                    amt = get_model("currency").convert(
                        amt, settings.currency_id.id,
                        line.track2_id.currency_id.id)
                vals = {
                    "date": obj.date,
                    "track_id": line.track2_id.id,
                    "amount": amt,
                    "description": line.description,
                    "move_id": obj.id,
                }
                get_model("account.track.entry").create(vals)

    def void(self, ids, context={}):
        obj = self.browse(ids[0])
        settings = get_model("settings").browse(1)
        if settings.lock_date:
            if obj.date < settings.lock_date:
                raise Exception("Accounting transaction is before lock date")
        obj.lines.unreconcile()
        obj.write({"state": "voided"})
        obj.delete_track_entries()
        get_model("field.cache").clear_cache(model="account.account")
        get_model("account.balance").update_balances()

    def delete_track_entries(self, ids, context={}):
        obj = self.browse(ids[0])
        obj.track_entries.delete()

    def get_total(self, ids, context):
        vals = {}
        for obj in self.browse(ids):
            total_debit = 0
            total_credit = 0
            for line in obj.lines:
                total_debit += line.debit
                total_credit += line.credit
            vals[obj.id] = {
                "total_debit": total_debit,
                "total_credit": total_credit,
            }
        return vals

    def update_amounts(self, context):
        data = context["data"]
        data["total_debit"] = 0
        data["total_credit"] = 0
        for line in data["lines"]:
            if not line:
                continue
            debit = line.get("debit") or 0
            credit = line.get("credit") or 0
            data["total_debit"] += debit
            data["total_credit"] += credit
            if line.get("debit") is not None and line.get("credit") is None:
                line["credit"] = 0
            if line.get("credit") is not None and line.get("debit") is None:
                line["debit"] = 0
        data["difference"] = data["total_debit"] - data["total_credit"]
        return data

    def get_line_desc(self, context):
        data = context["data"]
        path = context["path"]
        if not data.get("default_line_desc"):
            return
        if not get_data_path(data, path):
            set_data_path(data, path, data.get("narration"))
        return data

    def view_journal(self, ids, context={}):
        res = self.read(ids, ["related_id"])[0]["related_id"]
        rel = res and res[0] or None
        next = None
        if rel:
            model, model_id = rel.split(",")
            if model == "account.invoice":
                next = {
                    "name": "view_invoice",
                    "active_id": model_id,
                }
            elif model == "account.payment":
                next = {
                    "name": "payment",
                    "mode": "form",
                    "active_id": model_id,
                }
            elif model == "account.transfer":
                next = {
                    "name": "bank_transfer",
                    "mode": "form",
                    "active_id": model_id,
                }
            elif model == "account.claim":
                next = {
                    "name": "account_claim_edit",
                    "active_id": model_id,
                }
        if not next:
            next = {
                "name": "journal_entry",
                "mode": "form",
                "active_id": ids[0],
            }
        return {"next": next}

    def copy(self, ids, context={}):
        obj = self.browse(ids)[0]
        vals = {
            "journal_id": obj.journal_id.id,
            "ref": obj.ref,
            "default_line_desc": obj.default_line_desc,
            "narration": obj.narration,
            "lines": [],
        }
        for line in obj.lines:
            line_vals = {
                "description": line.description,
                "account_id": line.account_id.id,
                "debit": line.debit,
                "credit": line.credit,
                "tax_comp_id": line.tax_comp_id.id,
                "tax_base": line.tax_base,
                "contact_id": line.contact_id.id,
                "track_id": line.track_id.id,
                "track2_id": line.track2_id.id,
            }
            vals["lines"].append(("create", line_vals))
        new_id = self.create(vals, context={"journal_id": obj.journal_id.id})
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": new_id,
            },
            "flash":
            "Journal entry %s copied to %s" % (obj.number, new_obj.number),
        }

    def to_draft(self, ids, context={}):
        obj = self.browse(ids)[0]
        for line in obj.lines:
            line.unreconcile()
        obj.write({"state": "draft"})
        obj.delete_track_entries()
        get_model("account.balance").update_balances()
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": obj.id,
            },
            "flash": "Journal entry #%d set to draft" % obj.id,
        }

    def onchange_journal(self, context={}):
        data = context["data"]
        journal_id = data["journal_id"]
        date = data["date"]
        number = self._get_number(context={
            "journal_id": journal_id,
            "date": date
        })
        data["number"] = number
        return data

    def onchange_date(self, context={}):
        data = context["data"]
        journal_id = data["journal_id"]
        date = data["date"]
        number = self._get_number(context={
            "journal_id": journal_id,
            "date": date
        })
        data["number"] = number
        return data

    def get_data(self, ids, context={}):
        company_id = get_active_company()
        comp = get_model("company").browse(company_id)
        settings = get_model('settings').browse(1)
        pages = []
        for obj in self.browse(ids):
            lines = []
            for line in obj.lines:
                lines.append({
                    'description': line.description,
                    'account_code': line.account_id.code,
                    'account_name': line.account_id.name,
                    'debit': line.debit,
                    'credit': line.credit,
                    'tax_comp': line.tax_comp_id.name,
                    'tax_base': line.tax_base,
                    'track': line.track_id.name,
                    'contact': line.contact_id.name,
                })
            data = {
                "comp_name": comp.name,
                "number": obj.number,
                "date": obj.date,
                "journal": obj.journal_id.name,
                "narration": obj.narration,
                "lines": lines,
                "total_debit": obj.total_debit,
                "total_credit": obj.total_credit,
            }
            if settings.logo:
                data['logo'] = get_file_path(settings.logo)
            pages.append(data)
        if pages:
            pages[-1]["is_last_page"] = True
        return {
            "pages": pages,
            "logo":
            get_file_path(settings.logo),  # XXX: remove when render_odt fixed
        }

    def reverse(self, ids, context={}):
        obj = self.browse(ids)[0]
        vals = {
            "journal_id": obj.journal_id.id,
            "ref": obj.ref,
            "default_line_desc": obj.default_line_desc,
            "narration": obj.narration,
            "lines": [],
        }
        for line in obj.lines:
            line_vals = {
                "description": line.description,
                "account_id": line.account_id.id,
                "debit": line.credit,
                "credit": line.debit,
                "tax_comp_id": line.tax_comp_id.id,
                "tax_base": line.tax_base,
                "contact_id": line.contact_id.id,
                "track_id": line.track_id.id,
                "track2_id": line.track2_id.id,
            }
            vals["lines"].append(("create", line_vals))
        new_id = self.create(vals, context={"journal_id": obj.journal_id.id})
        self.post([new_id])
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": new_id,
            },
            "flash":
            "Journal entry %s reversed to %s" % (obj.number, new_obj.number),
            "reverse_move_id":
            new_id,
        }
Example #3
0
class SaleQuot(Model):
    _name = "sale.quot"
    _string = "Quotation"
    _audit_log = True
    _name_field = "number"
    _key = ["number"]
    _multi_company = True
    _fields = {
        "number":
        fields.Char("Number", required=True, search=True),
        "ref":
        fields.Char("Ref", search=True),
        "contact_id":
        fields.Many2One("contact", "Contact", required=True, search=True),
        "date":
        fields.Date("Date", required=True, search=True),
        "exp_date":
        fields.Date("Valid Until"),
        "state":
        fields.Selection([("draft", "Draft"),
                          ("waiting_approval", "Awaiting Approval"),
                          ("approved", "Approved"), ("won", "Won"),
                          ("lost", "Lost"), ("revised", "Revised")],
                         "Status",
                         function="get_state",
                         store=True),
        "lines":
        fields.One2Many("sale.quot.line", "quot_id", "Lines"),
        "amount_subtotal":
        fields.Decimal("Subtotal",
                       function="get_amount",
                       function_multi=True,
                       store=True),
        "amount_tax":
        fields.Decimal("Tax Amount",
                       function="get_amount",
                       function_multi=True,
                       store=True),
        "amount_total":
        fields.Decimal("Total",
                       function="get_amount",
                       function_multi=True,
                       store=True),
        "amount_total_words":
        fields.Char("Total Words", function="get_amount_total_words"),
        "qty_total":
        fields.Decimal("Total", function="get_qty_total"),
        "currency_id":
        fields.Many2One("currency", "Currency", required=True),
        "opport_id":
        fields.Many2One("sale.opportunity", "Opportunity", search=True),
        "user_id":
        fields.Many2One("base.user", "Owner", search=True),
        "tax_type":
        fields.Selection([["tax_ex", "Tax Exclusive"],
                          ["tax_in", "Tax Inclusive"], ["no_tax", "No Tax"]],
                         "Tax Type",
                         required=True),
        "sales":
        fields.One2Many("sale.order", "quot_id", "Sales Orders"),
        "payment_terms":
        fields.Text("Payment Terms"),
        "other_info":
        fields.Text("Other Information"),
        "comments":
        fields.One2Many("message", "related_id", "Comments"),
        "activities":
        fields.One2Many("activity", "related_id", "Activities"),
        "documents":
        fields.One2Many("document", "related_id", "Documents"),
        "uuid":
        fields.Char("UUID"),
        "price_list_id":
        fields.Many2One("price.list", "Price List"),
        "emails":
        fields.One2Many("email.message", "related_id", "Emails"),
        "company_id":
        fields.Many2One("company", "Company"),
        "related_id":
        fields.Reference([["issue", "Issue"]], "Related To"),
        "ship_term_id":
        fields.Many2One("ship.term", "Shipping Terms"),
        "sequence_id":
        fields.Many2One("sequence", "Number Sequence"),
        "job_template_id":
        fields.Many2One("job.template", "Service Order Template"),
        "lost_sale_code_id":
        fields.Many2One("reason.code",
                        "Lost Sale Reason Code",
                        condition=[["type", "=", "lost_sale"]]),
        "agg_amount_total":
        fields.Decimal("Total Amount", agg_function=["sum", "amount_total"]),
        "agg_amount_subtotal":
        fields.Decimal("Total Amount w/o Tax",
                       agg_function=["sum", "amount_subtotal"]),
        "year":
        fields.Char("Year", sql_function=["year", "date"]),
        "quarter":
        fields.Char("Quarter", sql_function=["quarter", "date"]),
        "month":
        fields.Char("Month", sql_function=["month", "date"]),
        "week":
        fields.Char("Week", sql_function=["week", "date"]),
        "est_costs":
        fields.One2Many("quot.cost", "quot_id", "Costs"),
        "est_cost_amount":
        fields.Float("Estimated Cost Amount",
                     function="get_est_profit",
                     function_multi=True),
        "est_profit_amount":
        fields.Float("Estimated Profit Amount",
                     function="get_est_profit",
                     function_multi=True),
        "est_margin_percent":
        fields.Float("Estimated Margin %",
                     function="get_est_profit",
                     function_multi=True),
        "currency_rates":
        fields.One2Many("custom.currency.rate", "related_id",
                        "Currency Rates"),
    }

    def _get_number(self, context={}):
        seq_id = get_model("sequence").find_sequence(type="sale_quot")
        if not seq_id:
            return None
        while 1:
            num = get_model("sequence").get_next_number(seq_id,
                                                        context=context)
            if not num:
                return None
            user_id = get_active_user()
            set_active_user(1)
            res = self.search([["number", "=", num]])
            set_active_user(user_id)
            if not res:
                return num
            get_model("sequence").increment_number(seq_id, context=context)

    def _get_currency(self, context={}):
        settings = get_model("settings").browse(1)
        return settings.currency_id.id

    _defaults = {
        "state": "draft",
        "date": lambda *a: time.strftime("%Y-%m-%d"),
        "number": _get_number,
        "currency_id": _get_currency,
        "tax_type": "tax_ex",
        "user_id": lambda self, context: get_active_user(),
        "uuid": lambda *a: str(uuid.uuid4()),
        "company_id": lambda *a: get_active_company(),
    }
    _constraints = ["check_fields"]
    _order = "date desc"

    def check_fields(self, ids, context={}):
        for obj in self.browse(ids):
            if obj.state in ("waiting_approval", "approved"):
                if not obj.lines:
                    raise Exception("No lines in quotation")

    def create(self, vals, **kw):
        id = super().create(vals, **kw)
        self.function_store([id])
        return id

    def write(self, ids, vals, **kw):
        opport_ids = []
        for obj in self.browse(ids):
            if obj.opport_id:
                opport_ids.append(obj.opport_id.id)
        super().write(ids, vals, **kw)
        if opport_ids:
            get_model("sale.opportunity").function_store(opport_ids)
        self.function_store(ids)

    def function_store(self, ids, field_names=None, context={}):
        super().function_store(ids, field_names, context)
        opport_ids = []
        for obj in self.browse(ids):
            if obj.opport_id:
                opport_ids.append(obj.opport_id.id)
        if opport_ids:
            get_model("sale.opportunity").function_store(opport_ids)

    def get_amount(self, ids, context={}):
        res = {}
        for obj in self.browse(ids):
            vals = {}
            subtotal = 0
            tax = 0
            for line in obj.lines:
                if line.is_hidden:
                    continue
                if line.tax_id:
                    line_tax = get_model("account.tax.rate").compute_tax(
                        line.tax_id.id, line.amount, tax_type=obj.tax_type)
                else:
                    line_tax = 0
                tax += line_tax
                if obj.tax_type == "tax_in":
                    subtotal += (line.amount or 0) - line_tax
                else:
                    subtotal += line.amount or 0
            vals["amount_subtotal"] = subtotal
            vals["amount_tax"] = tax
            vals["amount_total"] = subtotal + tax
            res[obj.id] = vals
        return res

    def get_qty_total(self, ids, context={}):
        res = {}
        for obj in self.browse(ids):
            qty = sum([line.qty for line in obj.lines])
            res[obj.id] = qty or 0
        return res

    def submit_for_approval(self, ids, context={}):
        for obj in self.browse(ids):
            if obj.state != "draft":
                raise Exception("Invalid state")
            obj.write({"state": "waiting_approval"})
        self.trigger(ids, "submit_for_approval")

    def approve(self, ids, context={}):
        for obj in self.browse(ids):
            if obj.state not in ("draft", "waiting_approval"):
                raise Exception("Invalid state")
            obj.write({"state": "approved"})

    def update_amounts(self, context):
        print("update_amounts")
        data = context["data"]
        data["amount_subtotal"] = 0
        data["amount_tax"] = 0
        tax_type = data["tax_type"]
        for line in data["lines"]:
            if not line:
                continue
            amt = (line.get("qty") or 0) * (line.get("unit_price") or 0)
            if line.get("discount"):
                disc = amt * line["discount"] / Decimal(100)
                amt -= disc
            else:
                disc = 0
            line["amount"] = amt
        hide_parents = []
        for line in data["lines"]:
            if not line:
                continue
            if line.get("sequence") and line.get("hide_sub"):
                hide_parents.append(line["sequence"])
        is_hidden = {}
        hide_totals = {}
        for line in data["lines"]:
            if not line:
                continue
            if not line.get("sequence"):
                continue
            parent_seq = None
            for seq in hide_parents:
                if line["sequence"].startswith(seq + "."):
                    parent_seq = seq
                    break
            if parent_seq:
                is_hidden[line["sequence"]] = True
                hide_totals.setdefault(parent_seq, 0)
                hide_totals[parent_seq] += line["amount"]
        for line in data["lines"]:
            if not line:
                continue
            if line.get("sequence") and line.get("hide_sub"):
                line["amount"] = hide_totals.get(line["sequence"], 0)
                if line["qty"]:
                    line["unit_price"] = line["amount"] / line["qty"]
        for line in data["lines"]:
            if is_hidden.get(line.get("sequence")):
                continue
            tax_id = line.get("tax_id")
            if tax_id:
                tax = get_model("account.tax.rate").compute_tax(
                    tax_id, line["amount"], tax_type=tax_type)
                data["amount_tax"] += tax
            else:
                tax = 0
            if tax_type == "tax_in":
                data["amount_subtotal"] += line["amount"] - tax
            else:
                data["amount_subtotal"] += line["amount"]
        data["amount_total"] = data["amount_subtotal"] + data["amount_tax"]
        return data

    def onchange_product(self, context):
        data = context["data"]
        contact_id = data.get("contact_id")
        if contact_id:
            contact = get_model("contact").browse(contact_id)
        else:
            contact = None
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if not prod_id:
            return {}
        prod = get_model("product").browse(prod_id)
        line["description"] = prod.description
        line["est_margin_percent_input"] = prod.gross_profit
        line["qty"] = 1
        if prod.uom_id is not None:
            line["uom_id"] = prod.uom_id.id
        pricelist_id = data["price_list_id"]
        price = None
        if pricelist_id:
            price = get_model("price.list").get_price(pricelist_id, prod.id, 1)
            price_list = get_model("price.list").browse(pricelist_id)
            price_currency_id = price_list.currency_id.id
        if price is None:
            price = prod.sale_price
            settings = get_model("settings").browse(1)
            price_currency_id = settings.currency_id.id
        if price is not None:
            currency_id = data["currency_id"]
            price_cur = get_model("currency").convert(price, price_currency_id,
                                                      currency_id)
            line["unit_price"] = price_cur
        if prod.sale_tax_id is not None:
            line["tax_id"] = prod.sale_tax_id.id
        data = self.update_amounts(context)
        return data

    def onchange_qty(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if not prod_id:
            return {}
        prod = get_model("product").browse(prod_id)
        pricelist_id = data["price_list_id"]
        qty = line["qty"]
        if line.get("unit_price") is None:
            price = None
            if pricelist_id:
                price = get_model("price.list").get_price(
                    pricelist_id, prod.id, qty)
                price_list = get_model("price.list").browse(pricelist_id)
                price_currency_id = price_list.currency_id.id
            if price is None:
                price = prod.sale_price
                settings = get_model("settings").browse(1)
                price_currency_id = settings.currency_id.id
            if price is not None:
                currency_id = data["currency_id"]
                price_cur = get_model("currency").convert(
                    price, price_currency_id, currency_id)
                line["unit_price"] = price_cur
        data = self.update_amounts(context)
        return data

    def onchange_contact(self, context):
        data = context["data"]
        contact_id = data.get("contact_id")
        if not contact_id:
            return {}
        contact = get_model("contact").browse(contact_id)
        data["payment_terms"] = contact.payment_terms
        data["price_list_id"] = contact.sale_price_list_id.id
        if contact.currency_id:
            data["currency_id"] = contact.currency_id.id
        else:
            settings = get_model("settings").browse(1)
            data["currency_id"] = settings.currency_id.id
        return data

    def onchange_uom(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if not prod_id:
            return {}
        prod = get_model("product").browse(prod_id)
        uom_id = line.get("uom_id")
        if not uom_id:
            return {}
        uom = get_model("uom").browse(uom_id)
        if prod.sale_price is not None:
            line[
                "unit_price"] = prod.sale_price * uom.ratio / prod.uom_id.ratio
        data = self.update_amounts(context)
        return data

    def copy(self, ids, context):
        obj = self.browse(ids)[0]
        vals = {
            "ref": obj.number,
            "contact_id": obj.contact_id.id,
            "currency_id": obj.currency_id.id,
            "tax_type": obj.tax_type,
            "payment_terms": obj.payment_terms,
            "other_info": obj.other_info,
            "exp_date": obj.exp_date,
            "opport_id": obj.opport_id.id,
            "lines": [],
        }
        for line in obj.lines:
            line_vals = {
                "product_id": line.product_id.id,
                "description": line.description,
                "qty": line.qty,
                "uom_id": line.uom_id.id,
                "unit_price": line.unit_price,
                "discount": line.discount,
                "tax_id": line.tax_id.id,
            }
            vals["lines"].append(("create", line_vals))
        new_id = self.create(vals, context=context)
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "quot",
                "mode": "form",
                "active_id": new_id,
            },
            "flash":
            "Quotation %s copied from %s" % (new_obj.number, obj.number),
        }

    def revise(self, ids, context):
        obj = self.browse(ids)[0]
        res = self.copy(ids, context)
        obj.write({"state": "revised"})
        return res

    def copy_to_sale_order(self, ids, context):
        id = ids[0]
        obj = self.browse(id)
        sale_vals = {
            "ref": obj.number,
            "quot_id": obj.id,
            "contact_id": obj.contact_id.id,
            "currency_id": obj.currency_id.id,
            "tax_type": obj.tax_type,
            "lines": [],
            "user_id": obj.user_id.id,
            "other_info": obj.other_info,
            "payment_terms": obj.payment_terms,
            "price_list_id": obj.price_list_id.id,
            "job_template_id": obj.job_template_id.id,
            "est_costs": [],
            "currency_rates": [],
        }
        for line in obj.lines:
            if not line.qty or not line.uom_id or not line.unit_price:
                continue
            prod = line.product_id
            line_vals = {
                "sequence": line.sequence,
                "product_id": prod.id,
                "description": line.description,
                "qty": line.qty,
                "uom_id": line.uom_id.id,
                "unit_price": line.unit_price if not line.is_hidden else 0,
                "discount": line.discount if not line.is_hidden else 0,
                "tax_id": line.tax_id.id if not line.is_hidden else None,
                "location_id": prod.location_id.id if prod else None,
            }
            sale_vals["lines"].append(("create", line_vals))
        for cost in obj.est_costs:
            cost_vals = {
                "sequence": cost.sequence,
                "product_id": cost.product_id.id,
                "description": cost.description,
                "supplier_id": cost.supplier_id.id,
                "list_price": cost.list_price,
                "purchase_price": cost.purchase_price,
                "purchase_duty_percent": cost.purchase_duty_percent,
                "purchase_ship_percent": cost.purchase_ship_percent,
                "landed_cost": cost.landed_cost,
                "qty": cost.qty,
                "currency_id": cost.currency_id.id,
            }
            sale_vals["est_costs"].append(("create", cost_vals))
        for r in obj.currency_rates:
            rate_vals = {
                "currency_id": r.currency_id.id,
                "rate": r.rate,
            }
            sale_vals["currency_rates"].append(("create", rate_vals))
        sale_id = get_model("sale.order").create(sale_vals, context=context)
        sale = get_model("sale.order").browse(sale_id)
        return {
            "next": {
                "name": "sale",
                "mode": "form",
                "active_id": sale_id,
            },
            "flash":
            "Sale order %s created from quotation %s" %
            (sale.number, obj.number)
        }

    def do_won(self, ids, context={}):
        for obj in self.browse(ids):
            assert obj.state == "approved"
            obj.write({"state": "won"})

    def do_lost(self, ids, context={}):
        for obj in self.browse(ids):
            assert obj.state == "approved"
            obj.write({"state": "lost"})

    def do_reopen(self, ids, context={}):
        for obj in self.browse(ids):
            assert obj.state in ("won", "lost")
            obj.write({"state": "approved"})

    def get_state(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            state = obj.state
            if state == "approved":
                found = False
                for sale in obj.sales:
                    if sale.state in ("confirmed", "done"):
                        found = True
                        break
                if found:
                    state = "won"
            vals[obj.id] = state
        return vals

    def view_link(self, ids, context={}):
        obj = self.browse(ids)[0]
        uuid = obj.uuid
        dbname = get_active_db()
        return {
            "next": {
                "type": "url",
                "url": "/view_quot?dbname=%s&uuid=%s" % (dbname, uuid),
            }
        }

    def get_template_quot_form(self, ids, context={}):
        obj = self.browse(ids)[0]
        has_discount = False
        for line in obj.lines:
            if line.discount:
                has_discount = True
        if has_discount:
            return "quot_form_disc"
        else:
            return "quot_form"

    def to_draft(self, ids, context={}):
        obj = self.browse(ids)[0]
        obj.write({"state": "draft"})

    def get_amount_total_words(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            amount_total_words = utils.num2word(obj.amount_total)
            vals[obj.id] = amount_total_words
            return vals

    def onchange_sequence(self, context={}):
        data = context["data"]
        seq_id = data["sequence_id"]
        if not seq_id:
            return None
        while 1:
            num = get_model("sequence").get_next_number(seq_id,
                                                        context=context)
            res = self.search([["number", "=", num]])
            if not res:
                break
            get_model("sequence").increment_number(seq_id, context=context)
        data["number"] = num
        return data

    def onchange_cost_product(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if prod_id:
            prod = get_model("product").browse(prod_id)
            line["description"] = prod.name
            line["list_price"] = prod.purchase_price
            line["purchase_price"] = prod.purchase_price
            line["landed_cost"] = prod.landed_cost
            line["qty"] = 1
            line["uom_id"] = prod.uom_id.id
            line["currency_id"] = prod.purchase_currency_id.id
            line["purchase_duty_percent"] = prod.purchase_duty_percent
            line["purchase_ship_percent"] = prod.purchase_ship_percent
            line["landed_cost"] = prod.landed_cost
            line["purcase_price"] = prod.purchase_price

            if prod.suppliers:
                line["supplier_id"] = prod.suppliers[0].supplier_id.id
        return data

    def get_est_profit(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            cost = 0
            for line in obj.lines:
                cost += line.est_cost_amount or 0
            profit = (obj.amount_subtotal or 0) - cost
            margin = profit * 100 / obj.amount_subtotal if obj.amount_subtotal else None
            vals[obj.id] = {
                "est_cost_amount": cost,
                "est_profit_amount": profit,
                "est_margin_percent": margin,
            }
        return vals

    def create_est_costs(self, ids, context={}):
        obj = self.browse(ids[0])
        del_ids = []
        for cost in obj.est_costs:
            if cost.product_id:
                del_ids.append(cost.id)
        get_model("quot.cost").delete(del_ids)
        #obj.write({"est_costs":[("delete_all",)]})
        for line in obj.lines:
            prod = line.product_id
            if not prod:
                continue
            if not prod.purchase_price:
                continue
            if not line.sequence:
                continue
            if "bundle" == prod.type:
                continue
            vals = {
                "quot_id":
                obj.id,
                "sequence":
                line.sequence if not line.is_hidden else line.parent_sequence,
                "product_id":
                prod.id,
                "description":
                prod.name,
                "supplier_id":
                prod.suppliers[0].supplier_id.id if prod.suppliers else None,
                "list_price":
                prod.purchase_price,
                "purchase_price":
                prod.purchase_price,
                "landed_cost":
                prod.landed_cost,
                "purchase_duty_percent":
                prod.purchase_duty_percent,
                "purchase_ship_percent":
                prod.purchase_ship_percent,
                "qty":
                line.qty,
                "currency_id":
                prod.purchase_currency_id.id,
            }
            get_model("quot.cost").create(vals)

    def merge_quotations(self, ids, context={}):
        if len(ids) < 2:
            raise Exception("Can not merge less than two quotations")
        contact_ids = []
        currency_ids = []
        tax_types = []
        for obj in self.browse(ids):
            contact_ids.append(obj.contact_id.id)
            currency_ids.append(obj.currency_id.id)
            tax_types.append(obj.tax_type)
        contact_ids = list(set(contact_ids))
        currency_ids = list(set(currency_ids))
        tax_types = list(set(tax_types))
        if len(contact_ids) > 1:
            raise Exception("Quotation customers have to be the same")
        if len(currency_ids) > 1:
            raise Exception("Quotation currencies have to be the same")
        if len(tax_types) > 1:
            raise Exception("Quotation tax types have to be the same")
        vals = {
            "contact_id": contact_ids[0],
            "currency_id": currency_ids[0],
            "tax_type": tax_types[0],
            "lines": [],
            "est_costs": [],
        }
        seq = 0
        for obj in self.browse(ids):
            seq_map = {}
            for line in obj.lines:
                seq += 1
                seq_map[line.sequence] = seq
                line_vals = {
                    "sequence": seq,
                    "product_id": line.product_id.id,
                    "description": line.description,
                    "qty": line.qty,
                    "uom_id": line.uom_id.id,
                    "unit_price": line.unit_price,
                    "discount": line.discount,
                    "tax_id": line.tax_id.id,
                }
                vals["lines"].append(("create", line_vals))
            for cost in obj.est_costs:
                cost_vals = {
                    "sequence": seq_map.get(cost.sequence),
                    "product_id": cost.product_id.id,
                    "description": cost.description,
                    "supplier_id": cost.supplier_id.id,
                    "list_price": cost.list_price,
                    "purchase_price": cost.purchase_price,
                    "landed_cost": cost.landed_cost,
                    "qty": cost.qty,
                    "currency_id": cost.currency_id.id,
                }
                vals["est_costs"].append(("create", cost_vals))
        new_id = self.create(vals, context=context)
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "quot",
                "mode": "form",
                "active_id": new_id,
            },
            "flash": "Quotations merged",
        }

    def onchange_est_margin(self, context={}):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        margin = line["est_margin_percent_input"]
        amt = line["est_cost_amount"] / (1 - margin / Decimal(100))
        price = round(amt / line["qty"])
        line["unit_price"] = price
        self.update_amounts(context)
        return data

    def get_relative_currency_rate(self, ids, currency_id):
        obj = self.browse(ids[0])
        rate = None
        for r in obj.currency_rates:
            if r.currency_id.id == currency_id:
                rate = r.rate
                break
        if rate is None:
            rate_from = get_model("currency").get_rate([currency_id],
                                                       obj.date) or Decimal(1)
            rate_to = obj.currency_id.get_rate(obj.date) or Decimal(1)
            rate = rate_from / rate_to
        return rate
Example #4
0
class SaleOrderLine(Model):
    _name = "sale.order.line"
    _name_field = "order_id"
    _fields = {
        "order_id": fields.Many2One("sale.order", "Sales Order", required=True, on_delete="cascade", search=True),
        "product_id": fields.Many2One("product", "Product", search=True),
        "description": fields.Text("Description", required=True, search=True),
        "qty": fields.Decimal("Qty"),
        "uom_id": fields.Many2One("uom", "UoM"),
        "unit_price": fields.Decimal("Unit Price", search=True, required=True, scale=6),
        "tax_id": fields.Many2One("account.tax.rate", "Tax Rate"),
        "amount": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True),
        "amount_cur": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True),
        "qty_stock": fields.Decimal("Qty (Stock UoM)"),
        "qty_delivered": fields.Decimal("Delivered Qty", function="get_qty_delivered"),
        "qty_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_invoiced"),
        "contact_id": fields.Many2One("contact", "Contact", function="_get_related", function_search="_search_related", function_context={"path": "order_id.contact_id"}, search=True),
        "date": fields.Date("Date", function="_get_related", function_search="_search_related", function_context={"path": "order_id.date"}, search=True),
        "user_id": fields.Many2One("base.user", "Owner", function="_get_related", function_search="_search_related", function_context={"path": "order_id.user_id"}, search=True),
        "amount_discount": fields.Decimal("Discount Amount", function="get_amount", function_multi=True),
        "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", function="_get_related", function_search="_search_related", function_context={"path": "order_id.state"}, search=True),
        "qty2": fields.Decimal("Secondary Qty"),
        "location_id": fields.Many2One("stock.location", "Location", condition=[["type", "=", "internal"]]),
        "product_categs": fields.Many2Many("product.categ", "Product Categories", function="_get_related", function_context={"path": "product_id.categs"}, function_search="_search_related", search=True),
        "product_categ_id": fields.Many2Many("product.categ", "Product Category", function="_get_related", function_context={"path": "product_id.categ_id"}, function_search="_search_related", search=True),
        "discount": fields.Decimal("Disc %"),  # XXX: rename to discount_percent later
        "discount_amount": fields.Decimal("Disc Amt"),
        "qty_avail": fields.Decimal("Qty In Stock", function="get_qty_avail"),
        "agg_amount": fields.Decimal("Total Amount", agg_function=["sum", "amount"]),
        "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum", "qty"]),
        "remark": fields.Char("Remark"),
        "ship_method_id": fields.Many2One("ship.method", "Shipping Method"),
        "sequence": fields.Char("Item No."),
        "due_date": fields.Date("Due Date"),
        "est_cost_amount": fields.Float("Est. Cost Amount",function="get_est_profit",function_multi=True),
        "est_profit_amount": fields.Float("Est. Profit Amount",function="get_est_profit",function_multi=True),
        "est_margin_percent": fields.Float("Est. Margin %",function="get_est_profit",function_multi=True),
        "act_cost_amount": fields.Float("Act. Cost Amount",function="get_act_profit",function_multi=True),
        "act_profit_amount": fields.Float("Act. Profit Amount",function="get_act_profit",function_multi=True,store=True),
        "act_margin_percent": fields.Float("Act. Margin %",function="get_act_profit",function_multi=True),
        "promotion_amount": fields.Decimal("Prom Amt",function="get_amount",function_multi=True),
        "agg_act_profit": fields.Decimal("Total Actual Profit", agg_function=["sum", "act_profit_amount"]),
        "production_id": fields.Many2One("production.order","Production Order"),
        "lot_id": fields.Many2One("stock.lot","Lot / Serial Number"),
        "ship_address_id": fields.Many2One("address", "Shipping Address"),
        "packaging_id": fields.Many2One("stock.packaging", "Packaging"),
        "delivery_slot_id": fields.Many2One("delivery.slot","Delivery Slot"),
        "ship_tracking": fields.Char("Tracking Numbers", function="get_ship_tracking"),
    }

    def create(self, vals, context={}):
        id = super(SaleOrderLine, self).create(vals, context)
        self.function_store([id])
        return id

    def write(self, ids, vals, context={}):
        super(SaleOrderLine, self).write(ids, vals, context)
        self.function_store(ids)

    def get_amount(self, ids, context={}):
        vals = {}
        settings = get_model("settings").browse(1)
        sale_ids=[]
        for line in self.browse(ids):
            sale_ids.append(line.order_id.id)
        sale_ids=list(set(sale_ids))
        for sale in get_model("sale.order").browse(sale_ids):
            prod_qtys={}
            prom_amts={}
            prom_pcts={}
            for line in sale.lines:
                prod_qtys.setdefault(line.product_id.id,0)
                prod_qtys[line.product_id.id]+=line.qty
            for line in sale.used_promotions:
                if line.amount and line.product_id:
                    prom_amts.setdefault(line.product_id.id,0)
                    prom_amts[line.product_id.id]+=line.amount
                elif line.percent:
                    prom_pcts.setdefault(line.product_id.id,0)
                    prom_pcts[line.product_id.id]+=line.percent
            for line in sale.lines:
                amt = line.qty * line.unit_price
                if line.discount:
                    disc = amt * line.discount / 100
                else:
                    disc = 0
                if line.discount_amount:
                    disc += line.discount_amount
                amt-=disc
                amt_before_prom=amt
                prom_amt=prom_amts.get(line.product_id.id,Decimal(0))/prod_qtys[line.product_id.id]*line.qty
                prom_pct=prom_pcts.get(line.product_id.id,Decimal(0))+prom_pcts.get(None,0)
                if prom_pct:
                    prom_amt+=math.ceil(amt_before_prom/line.qty*prom_pct/100)*line.qty
                if prom_amt:
                    amt-=prom_amt
                order = line.order_id
                vals[line.id] = {
                    "amount": amt,
                    "amount_cur": get_model("currency").convert(amt, order.currency_id.id, settings.currency_id.id),
                    "amount_discount": disc,
                    "promotion_amount": prom_amt,
                }
        return vals

    def get_qty_delivered(self, ids, context={}):
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            delivered_qtys = {}
            for move in order.stock_moves:
                if move.state != "done":
                    continue
                prod_id = move.product_id.id
                k = (prod_id, move.location_from_id.id)
                delivered_qtys.setdefault(k, 0)
                delivered_qtys[k] += move.qty  # XXX: uom
                k = (prod_id, move.location_to_id.id)
                delivered_qtys.setdefault(k, 0)
                delivered_qtys[k] -= move.qty  # XXX: uom
            for line in order.lines:
                k = (line.product_id.id, line.location_id.id)
                delivered_qty = delivered_qtys.get(k, 0)  # XXX: uom
                used_qty = min(line.qty, delivered_qty)
                vals[line.id] = used_qty
                if k in delivered_qtys:
                    delivered_qtys[k] -= used_qty
            for line in reversed(order.lines):
                k = (line.product_id.id, line.location_id.id)
                remain_qty = delivered_qtys.get(k, 0)  # XXX: uom
                if remain_qty:
                    vals[line.id] += remain_qty
                    delivered_qtys[k] -= remain_qty
        vals = {x: vals[x] for x in ids}
        return vals

    def get_qty_invoiced(self, ids, context={}):
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            inv_qtys = {}
            for inv in order.invoices:
                if inv.state not in ("draft","waiting_payment","paid"):
                    continue
                for line in inv.lines:
                    prod_id = line.product_id.id
                    inv_qtys.setdefault(prod_id, 0)
                    inv_qtys[prod_id] += line.qty
            for line in order.lines:
                if line.id not in ids:
                    continue
                prod_id = line.product_id.id
                inv_qty = inv_qtys.get(prod_id, 0)  # XXX: uom
                used_qty = min(line.qty, inv_qty)
                vals[line.id] = used_qty
                if prod_id in inv_qtys:
                    inv_qtys[prod_id] -= used_qty
            for line in reversed(order.lines):
                prod_id = line.product_id.id
                remain_qty = inv_qtys.get(prod_id, 0)  # XXX: uom
                if remain_qty:
                    vals[line.id] += remain_qty
                    inv_qtys[prod_id] -= remain_qty
        vals = {x: vals[x] for x in ids}
        return vals

    def get_qty_avail(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            prod_id = obj.product_id.id
            loc_id = obj.location_id.id
            if prod_id and loc_id:
                res = get_model("stock.location").compute_balance([loc_id], prod_id)
                qty = res["bal_qty"]
            else:
                qty = None
            vals[obj.id] = qty
        return vals

    def get_est_profit(self,ids,context={}):
        sale_ids=[]
        for line in self.browse(ids):
            sale_ids.append(line.order_id.id)
        sale_ids=list(set(sale_ids))
        item_costs={}
        for sale in get_model("sale.order").browse(sale_ids):
            for cost in sale.est_costs:
                k=(sale.id,cost.sequence)
                if k not in item_costs:
                    item_costs[k]=0
                amt=cost.amount or 0
                if cost.currency_id:
                    rate=sale.get_relative_currency_rate(cost.currency_id.id)
                    amt=amt*rate
                item_costs[k]+=amt
        vals={}
        for line in self.browse(ids):
            k=(line.order_id.id,line.sequence)
            cost=item_costs.get(k,0)
            profit=line.amount-cost
            margin=profit*100/line.amount if line.amount else None
            vals[line.id]={
                "est_cost_amount": cost,
                "est_profit_amount": profit,
                "est_margin_percent": margin,
            }
        return vals

    def get_act_profit(self,ids,context={}):
        sale_ids=[]
        for line in self.browse(ids):
            sale_ids.append(line.order_id.id)
        sale_ids=list(set(sale_ids))
        item_costs={}
        for sale in get_model("sale.order").browse(sale_ids):
            for line in sale.track_entries:
                k=(sale.id,line.track_id.code)
                if k not in item_costs:
                    item_costs[k]=0
                # TODO: convert currency
                item_costs[k]-=line.amount
        vals={}
        for line in self.browse(ids):
            track_code="%s / %s"%(line.order_id.number,line.sequence)
            k=(line.order_id.id,track_code)
            cost=item_costs.get(k,0)
            profit=line.amount-cost
            margin=profit*100/line.amount if line.amount else None
            vals[line.id]={
                "act_cost_amount": cost,
                "act_profit_amount": profit,
                "act_margin_percent": margin,
            }
        return vals

    def get_ship_tracking(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            track_nos = []
            if obj.due_date:
                for pick in obj.order_id.pickings:
                    if pick.date[:10]==obj.due_date and pick.ship_tracking:
                        track_nos.append(pick.ship_tracking)
            vals[obj.id] = ", ".join(track_nos)
        return vals
Example #5
0
class SaleOrderLine(Model):
    _name = "sale.order.line"
    _name_field = "order_id"
    _fields = {
        "order_id": fields.Many2One("sale.order", "Sales Order", required=True, on_delete="cascade", search=True),
        "product_id": fields.Many2One("product", "Product", search=True),
        "description": fields.Text("Description", required=True, search=True),
        "qty": fields.Decimal("Qty"),
        "uom_id": fields.Many2One("uom", "UoM"),
        "unit_price": fields.Decimal("Unit Price", search=True, required=True, scale=6),
        "tax_id": fields.Many2One("account.tax.rate", "Tax Rate"),
        "amount": fields.Decimal("Amount", function="get_amount", function_multi=True, store=True, function_order=1, search=True),
        "amount_cur": fields.Decimal("Amount (Cur)", function="get_amount", function_multi=True, store=True, function_order=1, search=True),
        "qty_stock": fields.Decimal("Qty (Stock UoM)"),
        "qty_delivered": fields.Decimal("Delivered Qty", function="get_qty_delivered"),
        "qty_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_invoiced"),
        "unit_amount_invoiced": fields.Decimal("UNIT amount invoiced", function="get_unit_amount_invoiced"),
        "qty_service_invoiced": fields.Decimal("Invoiced Qty", function="get_qty_service_invoiced"),
        "unit_service_amount_invoiced": fields.Decimal("service unit amount invoiced", function="get_service_unit_amount_invoiced"),
        "contact_id": fields.Many2One("contact", "Contact", function="_get_related", function_search="_search_related", function_context={"path": "order_id.contact_id"}, search=True),
        "date": fields.Date("Date", function="_get_related", function_search="_search_related", function_context={"path": "order_id.date"}, search=True),
        "user_id": fields.Many2One("base.user", "Owner", function="_get_related", function_search="_search_related", function_context={"path": "order_id.user_id"}, search=True),
        "amount_discount": fields.Decimal("Discount Amount", function="get_amount", function_multi=True),
        "state": fields.Selection([("draft", "Draft"), ("confirmed", "Confirmed"), ("done", "Completed"), ("voided", "Voided")], "Status", function="_get_related", function_search="_search_related", function_context={"path": "order_id.state"}, search=True),
        "qty2": fields.Decimal("Secondary Qty"),
        "location_id": fields.Many2One("stock.location", "Location", condition=[["type", "=", "internal"]]),
        "product_categ_id": fields.Many2Many("product.categ", "Product Category", function="_get_related", function_context={"path": "product_id.categ_id"}, function_search="_search_related", search=True),
        "discount": fields.Decimal("Disc %"),  # XXX: rename to discount_percent later
        "discount_amount": fields.Decimal("Disc Amt"),
        "qty_avail": fields.Decimal("Qty In Stock", function="get_qty_avail"),
        "agg_amount": fields.Decimal("Total Amount", agg_function=["sum", "amount"]),
        "agg_amount_cur": fields.Decimal("Total Amount Cur", agg_function=["sum", "amount_cur"]),
        "agg_qty": fields.Decimal("Total Order Qty", agg_function=["sum", "qty"]),
        "remark": fields.Char("Remark"),
        "ship_method_id": fields.Many2One("ship.method", "Shipping Method"),
        "sequence": fields.Char("Item No."),
        "est_cost_amount": fields.Float("Est. Cost Amount",function="get_est_profit",function_multi=True),
        "est_profit_amount": fields.Float("Est. Profit Amount",function="get_est_profit",function_multi=True),
        "est_margin_percent": fields.Float("Est. Margin %",function="get_est_profit",function_multi=True),
        "act_cost_amount": fields.Float("Act. Cost Amount",function="get_act_profit",function_multi=True),
        "act_profit_amount": fields.Float("Act. Profit Amount",function="get_act_profit",function_multi=True),
        "act_margin_percent": fields.Float("Act. Margin %",function="get_act_profit",function_multi=True),
        "promotion_amount": fields.Decimal("Prom Amt",function="get_amount",function_multi=True),
        "agg_act_profit": fields.Decimal("Total Actual Profit", agg_function=["sum", "act_profit_amount"]),
        "production_id": fields.Many2One("production.order","Production Order"),
    }

    _order_expression="case when tbl0.sequence is not null then (substring(tbl0.sequence, '^[0-9]+'))::int else tbl0.id end, tbl0.sequence"

    def get_service_unit_amount_invoiced(self, ids, context={}): #this will get qty based on the description
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            inv_amounts = {}
            for inv in order.invoices:
                if inv.state not in ("draft","waiting_payment","paid"):
                    continue
                for line in inv.lines:
                    prod_id = line.description # assume description as the product and fitler
                    inv_amounts.setdefault(prod_id, 0)
                    inv_amounts[prod_id] += line.amount or 0
            for line in order.lines:
                if line.id not in ids:
                    continue
                prod_id = line.description
                inv_amount = inv_amounts.get(prod_id, 0)  # XXX: uom
                used_amount = min(line.amount, inv_amount)
                vals[line.id] = used_amount
                if prod_id in inv_amounts:
                    inv_amounts[prod_id] -= used_amount
            for line in reversed(order.lines):
                prod_id = line.description
                remain_amount = inv_amounts.get(prod_id, 0)  # XXX: uom
                if remain_amount:
                    vals[line.id] += remain_amount
                    inv_amounts[prod_id] -= remain_amount
        vals = {x: vals[x] for x in ids}
        return vals

    def get_unit_amount_invoiced(self, ids, context={}):
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            inv_amounts = {}
            for inv in order.invoices:
                if inv.state not in ("draft","waiting_payment","paid"):
                    continue
                for line in inv.lines:
                    prod_id = line.product_id.id
                    inv_amounts.setdefault(prod_id, 0)
                    inv_amounts[prod_id] += line.amount or 0
            for line in order.lines:
                if line.id not in ids:
                    continue
                prod_id = line.product_id.id
                inv_amount = inv_amounts.get(prod_id, 0)  # XXX: uom
                used_amount = min(line.amount, inv_amount)
                vals[line.id] = used_amount
                if prod_id in inv_amounts:
                    inv_amounts[prod_id] -= used_amount
            for line in reversed(order.lines):
                prod_id = line.product_id.id
                remain_amount = inv_amounts.get(prod_id, 0)  # XXX: uom
                if remain_amount:
                    vals[line.id] += remain_amount
                    inv_amounts[prod_id] -= remain_amount
        vals = {x: vals[x] for x in ids}
        return vals

    def get_qty_service_invoiced(self, ids, context={}): #this will get qty based on the description
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            inv_qtys = {}
            for inv in order.invoices:
                if inv.state not in ("draft","waiting_payment","paid"):
                    continue
                for line in inv.lines:
                    prod_id = line.description # assume description as the product and fitler
                    inv_qtys.setdefault(prod_id, 0)
                    inv_qtys[prod_id] += line.qty or 0
            for line in order.lines:
                if line.id not in ids:
                    continue
                prod_id = line.description
                inv_qty = inv_qtys.get(prod_id, 0)  # XXX: uom
                used_qty = min(line.qty, inv_qty)
                vals[line.id] = used_qty
                if prod_id in inv_qtys:
                    inv_qtys[prod_id] -= used_qty
            for line in reversed(order.lines):
                prod_id = line.description
                remain_qty = inv_qtys.get(prod_id, 0)  # XXX: uom
                if remain_qty:
                    vals[line.id] += remain_qty
                    inv_qtys[prod_id] -= remain_qty
        vals = {x: vals[x] for x in ids}
        return vals

    def create(self, vals, context={}):
        if not vals.get('qty_stock'):
            product_id=vals.get('product_id')
            location_id=vals.get('location_id')
            if product_id and location_id:
                vals['qty_stock']=get_model('stock.balance').get_qty_stock(product_id, location_id)
        id = super(SaleOrderLine, self).create(vals, context)
        self.function_store([id])
        return id

    def write(self, ids, vals, context={}):
        super(SaleOrderLine, self).write(ids, vals, context)
        self.function_store(ids)

    def get_amount(self, ids, context={}):
        vals = {}
        settings = get_model("settings").browse(1)
        sale_ids=[]
        for line in self.browse(ids):
            sale_ids.append(line.order_id.id)
        sale_ids=list(set(sale_ids))
        for sale in get_model("sale.order").browse(sale_ids):
            prod_qtys={}
            prom_amts={}
            prom_pcts={}
            for line in sale.lines:
                prod_qtys.setdefault(line.product_id.id,0)
                prod_qtys[line.product_id.id]+=line.qty
            for line in sale.used_promotions:
                if line.amount and line.product_id:
                    prom_amts.setdefault(line.product_id.id,0)
                    prom_amts[line.product_id.id]+=line.amount
                elif line.percent:
                    prom_pcts.setdefault(line.product_id.id,0)
                    prom_pcts[line.product_id.id]+=line.percent
            for line in sale.lines:
                amt = line.qty * line.unit_price
                amt = roundup(amt)
                if line.discount:
                    disc = amt * line.discount / 100
                else:
                    disc = 0
                if line.discount_amount:
                    disc += line.discount_amount
                amt-=disc
                amt_before_prom=amt
                prom_amt=prom_amts.get(line.product_id.id,Decimal(0))/prod_qtys[line.product_id.id]*line.qty
                prom_pct=prom_pcts.get(line.product_id.id,Decimal(0))+prom_pcts.get(None,0)
                if prom_pct:
                    prom_amt+=math.ceil(amt_before_prom/line.qty*prom_pct/100)*line.qty
                if prom_amt:
                    amt-=prom_amt
                order = line.order_id
                new_cur=get_model("currency").convert(amt, order.currency_id.id, settings.currency_id.id, rate_type="sell", date=sale.date)
                vals[line.id] = {
                    "amount": roundup(amt),
                    "amount_discount": disc,
                    "promotion_amount": prom_amt,
                    "amount_cur": new_cur and new_cur or None,
                }
        return vals

    def get_qty_delivered(self, ids, context={}):
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            delivered_qtys = {}
            for move in order.stock_moves:
                if move.state != "done":
                    continue
                prod_id = move.product_id.id
                k = (prod_id, move.location_from_id.id)
                delivered_qtys.setdefault(k, 0)
                delivered_qtys[k] += move.qty  # XXX: uom
                k = (prod_id, move.location_to_id.id)
                delivered_qtys.setdefault(k, 0)
                delivered_qtys[k] -= move.qty  # XXX: uom
            for job in order.jobs:
                for move in job.stock_moves:
                    if move.state != "done":
                        continue
                    prod_id = move.product_id.id
                    k = (prod_id, move.location_from_id.id)
                    delivered_qtys.setdefault(k, 0)
                    delivered_qtys[k] += move.qty  # XXX: uom
                    k = (prod_id, move.location_to_id.id)
                    delivered_qtys.setdefault(k, 0)
                    delivered_qtys[k] -= move.qty  # XXX: uom
            for line in order.lines:
                k = (line.product_id.id, line.location_id.id)
                delivered_qty = delivered_qtys.get(k, 0)  # XXX: uom
                used_qty = min(line.qty, delivered_qty)
                vals[line.id] = used_qty
                if k in delivered_qtys:
                    delivered_qtys[k] -= used_qty
            for line in reversed(order.lines):
                k = (line.product_id.id, line.location_id.id)
                remain_qty = delivered_qtys.get(k, 0)  # XXX: uom
                if remain_qty:
                    vals[line.id] += remain_qty
                    delivered_qtys[k] -= remain_qty
        vals = {x: vals[x] for x in ids}
        return vals

    def get_qty_invoiced(self, ids, context={}):
        order_ids = []
        for obj in self.browse(ids):
            order_ids.append(obj.order_id.id)
        order_ids = list(set(order_ids))
        vals = {}
        for order in get_model("sale.order").browse(order_ids):
            inv_qtys = {}
            for inv in order.invoices:
                if inv.state not in ("draft","waiting_payment","paid"):
                    continue
                for line in inv.lines:
                    prod_id = line.product_id.id
                    inv_qtys.setdefault(prod_id, 0)
                    inv_qtys[prod_id] += line.qty or 0
            for job in order.jobs:
                for inv in job.invoices:
                    if inv.state not in ("draft","waiting_payment","paid"):
                        continue
                    for line in inv.lines:
                        prod_id = line.product_id.id
                        inv_qtys.setdefault(prod_id, 0)
                        inv_qtys[prod_id] += line.qty or 0
            for line in order.lines:
                if line.id not in ids:
                    continue
                prod_id = line.product_id.id
                inv_qty = inv_qtys.get(prod_id, 0)  # XXX: uom
                used_qty = min(line.qty, inv_qty)
                vals[line.id] = used_qty
                if prod_id in inv_qtys:
                    inv_qtys[prod_id] -= used_qty
            for line in reversed(order.lines):
                prod_id = line.product_id.id
                remain_qty = inv_qtys.get(prod_id, 0)  # XXX: uom
                if remain_qty:
                    vals[line.id] += remain_qty
                    inv_qtys[prod_id] -= remain_qty
        vals = {x: vals[x] for x in ids}
        return vals

    def get_qty_avail(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            prod_id = obj.product_id.id
            loc_id = obj.location_id.id
            if prod_id and loc_id:
                res = get_model("stock.location").compute_balance([loc_id], prod_id)
                qty = res["bal_qty"]
            else:
                qty = None
            vals[obj.id] = qty
        return vals

    def get_est_profit(self,ids,context={}):
        sale_ids=[]
        settings = get_model('settings').browse(1)
        currency_id = settings.currency_id.id
        for line in self.browse(ids):
            sale_ids.append(line.order_id.id)
        sale_ids=list(set(sale_ids))
        item_costs={}
        for sale in get_model("sale.order").browse(sale_ids):
            for cost in sale.est_costs:
                k=(sale.id,cost.sequence)
                if k not in item_costs:
                    item_costs[k]=0
                if cost.currency_id == currency_id:
                    item_costs[k]+=cost.amount or 0
                else:
                    item_costs[k]+=cost.amount_conv or 0
        vals={}
        for line in self.browse(ids):
            profit=0
            k=(line.order_id.id,line.sequence)
            cost=item_costs.get(k,0)
            profit=line.amount-cost
            margin=(profit/line.amount)*100 if line.amount else 0
            vals[line.id]={
                "est_cost_amount": cost,
                "est_profit_amount": profit,
                "est_margin_percent": margin,
            }
        return vals

    def get_act_profit(self,ids,context={}):
        sale_ids=[]
        for line in self.browse(ids):
            sale_ids.append(line.order_id.id)
        sale_ids=list(set(sale_ids))
        item_costs={}
        for sale in get_model("sale.order").browse(sale_ids):
            for line in sale.track_entries:
                k=(sale.id,line.track_id.code)
                if k not in item_costs:
                    item_costs[k]=0
                # TODO: convert currency
                item_costs[k]-=line.amount
        vals={}
        for line in self.browse(ids):
            track_code="%s / %s"%(line.order_id.number,line.sequence)
            k=(line.order_id.id,track_code)
            cost=item_costs.get(k,0)
            profit=line.amount-cost
            margin=profit*100/line.amount if line.amount else None
            vals[line.id]={
                "act_cost_amount": cost,
                "act_profit_amount": profit,
                "act_margin_percent": margin,
            }
        return vals
Example #6
0
class Campaign(Model):
    _name = "mkt.campaign"
    _string = "Campaign"
    _fields = {
        "name":
        fields.Char("Campaign Name", required=True, search=True),
        "date":
        fields.Date("Date", required=True, search=True),
        "target_lists":
        fields.Many2Many("mkt.target.list", "Target Lists"),
        "email_tmpl_id":
        fields.Many2One("email.template", "Email Template"),
        "mailbox_id":
        fields.Many2One("email.mailbox", "Email Mailbox"),
        "comments":
        fields.One2Many("message", "related_id", "Comments"),
        "state":
        fields.Selection([["active", "Active"], ["inactive", "Inactive"]],
                         "Status",
                         required=True),
        "limit_day":
        fields.Integer("Daily Limit"),
        "limit_hour":
        fields.Integer("Hourly Limit"),
        "num_targets":
        fields.Integer("Number targets",
                       function="get_stats",
                       function_multi=True),
        "num_create":
        fields.Integer("Number emails created",
                       function="get_stats",
                       function_multi=True),
        "percent_create":
        fields.Float("% created", function="get_stats", function_multi=True),
        "num_sent":
        fields.Integer("Number emails sent",
                       function="get_stats",
                       function_multi=True),
        "percent_sent":
        fields.Float("% sent", function="get_stats", function_multi=True),
        "num_delivered":
        fields.Integer("Number emails delivered",
                       function="get_stats",
                       function_multi=True),
        "percent_delivered":
        fields.Float("% delivered", function="get_stats", function_multi=True),
        "num_bounced":
        fields.Integer("Number emails bounced",
                       function="get_stats",
                       function_multi=True),
        "percent_bounced":
        fields.Float("% bounced", function="get_stats", function_multi=True),
        "num_rejected":
        fields.Integer("Number emails rejected",
                       function="get_stats",
                       function_multi=True),
        "percent_rejected":
        fields.Float("% rejected", function="get_stats", function_multi=True),
        "num_opened":
        fields.Integer("Number emails opened",
                       function="get_stats",
                       function_multi=True),
        "percent_opened":
        fields.Float("% opened", function="get_stats", function_multi=True),
        "num_clicked":
        fields.Integer("Number emails clicked",
                       function="get_stats",
                       function_multi=True),
        "percent_clicked":
        fields.Float("% clicked", function="get_stats", function_multi=True),
        "num_create_day":
        fields.Integer("Emails created within day",
                       function="get_stats",
                       function_multi=True),
        "num_create_hour":
        fields.Integer("Emails created within hour",
                       function="get_stats",
                       function_multi=True),
        "emails":
        fields.One2Many("email.message", "related_id", "Emails"),
        "min_target_life":
        fields.Integer("Minimum Target Life (days)"),
    }
    _defaults = {
        "date": lambda *a: time.strftime("%Y-%m-%d"),
        "state": "active",
    }

    def create_emails_all(self, context={}):
        for obj in self.search_browse([["state", "=", "active"]]):
            obj.create_emails()

    def create_emails(self, ids, context={}):
        obj = self.browse(ids)[0]
        if obj.state != "active":
            raise Exception("Invalid state")
        if not obj.email_tmpl_id:
            raise Exception("Missing email template")
        limit = None
        if obj.limit_day:
            limit = obj.limit_day - obj.num_create_day
        if obj.limit_hour:
            l = obj.limit_hour - obj.num_create_hour
            if limit is None or l < limit:
                limit = l
        sent_emails = set()
        for email in obj.emails:
            if not email.name_id:
                continue
            if email.name_id._model != "mkt.target":
                continue
            target_id = email.name_id.id
            res = get_model("mkt.target").search(
                [["id", "=", email.name_id.id]])  # XXX
            if not res:
                continue
            target = get_model("mkt.target").browse(target_id)
            sent_emails.add(target.email)
        count = 0
        for tl in obj.target_lists:
            for target in tl.targets:
                if target.email in sent_emails:
                    continue
                if obj.min_target_life and target.target_life < obj.min_target_life:
                    continue
                if limit is not None and count >= limit:
                    break
                settings = get_model("settings").browse(1)
                data = {
                    "settings": settings,
                    "obj": target,
                }
                obj.email_tmpl_id.create_email(
                    data,
                    name_id="mkt.target,%d" % target.id,
                    related_id="mkt.campaign,%d" % obj.id,
                    mailbox_id=obj.mailbox_id)
                count += 1
                db = get_connection()
                db.commit()
        return {
            "next": {
                "name": "campaign",
                "mode": "form",
                "active_id": obj.id,
            },
            "flash": "%d emails created" % count,
        }

    def get_stats(self, ids, context={}):
        vals = {}
        for obj_id in ids:
            vals[obj_id] = {
                "num_targets": 0,
                "num_create": 0,
                "num_sent": 0,
                "num_delivered": 0,
                "num_bounced": 0,
                "num_rejected": 0,
                "num_opened": 0,
                "num_clicked": 0,
                "num_create_day": 0,
                "num_create_hour": 0,
            }
        db = get_connection()
        res = db.query(
            "SELECT c.id,COUNT(DISTINCT t.email) FROM mkt_campaign c JOIN m2m_mkt_campaign_mkt_target_list r ON r.mkt_campaign_id=c.id JOIN mkt_target t ON t.list_id=r.mkt_target_list_id WHERE c.id IN %s GROUP BY c.id",
            tuple(ids))
        for r in res:
            vals[r.id]["num_targets"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_create"] = r.count
        d = (datetime.now() -
             timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S")
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND date>%s GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]), d)
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_create_day"] = r.count
        d = (datetime.now() - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S")
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND date>%s GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]), d)
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_create_hour"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='sent' GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_sent"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='delivered' GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_delivered"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='bounced' GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_bounced"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND state='rejected' GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_rejected"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND opened GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_opened"] = r.count
        res = db.query(
            "SELECT related_id,COUNT(*) FROM email_message WHERE related_id IN %s AND clicked GROUP BY related_id",
            tuple(["mkt.campaign,%d" % x for x in ids]))
        for r in res:
            obj_id = int(r.related_id.split(",")[1])
            v = vals[obj_id]
            v["num_clicked"] = r.count
        for obj in self.browse(ids):
            v = vals[obj.id]
            v["percent_create"] = v["num_create"] * 100.0 / v[
                "num_targets"] if v["num_targets"] else None
            v["percent_sent"] = v["num_sent"] * 100.0 / v["num_create"] if v[
                "num_create"] else None
            v["percent_delivered"] = v["num_delivered"] * 100.0 / v[
                "num_create"] if v["num_create"] else None
            v["percent_bounced"] = v["num_bounced"] * 100.0 / v[
                "num_create"] if v["num_create"] else None
            v["percent_rejected"] = v["num_rejected"] * 100.0 / v[
                "num_create"] if v["num_create"] else None
            v["percent_opened"] = v["num_opened"] * 100.0 / v[
                "num_create"] if v["num_create"] else None
            v["percent_clicked"] = v["num_clicked"] * 100.0 / v[
                "num_create"] if v["num_create"] else None
        return vals
Example #7
0
class SaleQuot(Model):
    _name = "sale.quot"
    _string = "Quotation"
    _audit_log = True
    _name_field = "number"
    _key = ["number", "company_id"]
    _multi_company = True
    _fields = {
        "number":
        fields.Char("Number", required=True, search=True),
        "ref":
        fields.Char("Ref", search=True),
        "contact_id":
        fields.Many2One("contact", "Contact", required=True, search=True),
        "date":
        fields.Date("Date", required=True, search=True),
        "exp_date":
        fields.Date("Valid Until"),
        "state":
        fields.Selection([("draft", "Draft"),
                          ("waiting_approval", "Awaiting Approval"),
                          ("approved", "Approved"), ("won", "Won"),
                          ("lost", "Lost"), ("revised", "Revised")],
                         "Status",
                         function="get_state",
                         store=True),
        "lines":
        fields.One2Many("sale.quot.line", "quot_id", "Lines"),
        "amount_subtotal":
        fields.Decimal("Subtotal",
                       function="get_amount",
                       function_multi=True,
                       store=True),
        "amount_tax":
        fields.Decimal("Tax Amount",
                       function="get_amount",
                       function_multi=True,
                       store=True),
        "amount_total":
        fields.Decimal("Total",
                       function="get_amount",
                       function_multi=True,
                       store=True),
        "amount_total_words":
        fields.Char("Total Words", function="get_amount_total_words"),
        "qty_total":
        fields.Decimal("Total", function="get_qty_total"),
        "currency_id":
        fields.Many2One("currency", "Currency", required=True),
        "opport_id":
        fields.Many2One("sale.opportunity", "Opportunity", search=True),
        "user_id":
        fields.Many2One("base.user", "Owner", search=True),
        "tax_type":
        fields.Selection([["tax_ex", "Tax Exclusive"],
                          ["tax_in", "Tax Inclusive"], ["no_tax", "No Tax"]],
                         "Tax Type",
                         required=True),
        "sales":
        fields.One2Many("sale.order", "quot_id", "Sales Orders"),
        "payment_terms":
        fields.Text("Payment Terms"),
        "other_info":
        fields.Text("Other Information"),
        "comments":
        fields.One2Many("message", "related_id", "Comments"),
        "activities":
        fields.One2Many("activity", "related_id", "Activities"),
        "documents":
        fields.One2Many("document", "related_id", "Documents"),
        "uuid":
        fields.Char("UUID"),
        "price_list_id":
        fields.Many2One("price.list", "Price List"),
        "emails":
        fields.One2Many("email.message", "related_id", "Emails"),
        "company_id":
        fields.Many2One("company", "Company"),
        "related_id":
        fields.Reference([["issue", "Issue"]], "Related To"),
        "ship_term_id":
        fields.Many2One("ship.term", "Shipping Terms"),
        "sequence_id":
        fields.Many2One("sequence", "Number Sequence"),
        "job_template_id":
        fields.Many2One("job.template", "Service Order Template"),
        "lost_sale_code_id":
        fields.Many2One("reason.code",
                        "Lost Sale Reason Code",
                        condition=[["type", "=", "lost_sale"]]),
        "agg_amount_total":
        fields.Decimal("Total Amount", agg_function=["sum", "amount_total"]),
        "agg_amount_subtotal":
        fields.Decimal("Total Amount w/o Tax",
                       agg_function=["sum", "amount_subtotal"]),
        "year":
        fields.Char("Year", sql_function=["year", "date"]),
        "quarter":
        fields.Char("Quarter", sql_function=["quarter", "date"]),
        "month":
        fields.Char("Month", sql_function=["month", "date"]),
        "week":
        fields.Char("Week", sql_function=["week", "date"]),
        "est_costs":
        fields.One2Many("quot.cost", "quot_id", "Costs"),
        "est_cost_amount":
        fields.Float("Estimated Cost Amount",
                     function="get_est_profit",
                     function_multi=True),
        "est_profit_amount":
        fields.Float("Estimated Profit Amount",
                     function="get_est_profit",
                     function_multi=True),
        "est_margin_percent":
        fields.Float("Estimated Margin %",
                     function="get_est_profit",
                     function_multi=True),
        "est_cost_amount_conv":
        fields.Float("Estimated Cost Amount (Cur)",
                     function="get_est_profit",
                     function_multi=True),
        "est_profit_amount_conv":
        fields.Float("Estimated Profit Amount (Cur)",
                     function="get_est_profit",
                     function_multi=True),
        "est_margin_percent_conv":
        fields.Float("Estimated Margin % (Cur)",
                     function="get_est_profit",
                     function_multi=True),
        "est_sale_amount_conv":
        fields.Float("Sale Amount (Cur)",
                     function="get_est_profit",
                     function_multi=True),
        "est_sale_amount":
        fields.Float("Sale Amount",
                     function="get_est_profit",
                     function_multi=True),
        "currency_rates":
        fields.One2Many("custom.currency.rate", "related_id",
                        "Currency Rates"),
    }

    def _get_number(self, context={}):
        seq_id = get_model("sequence").find_sequence(type="sale_quot")
        if not seq_id:
            return None
        while 1:
            num = get_model("sequence").get_next_number(seq_id,
                                                        context=context)
            if not num:
                return None
            user_id = get_active_user()
            set_active_user(1)
            res = self.search([["number", "=", num]])
            set_active_user(user_id)
            if not res:
                return num
            get_model("sequence").increment_number(seq_id, context=context)

    def _get_currency(self, context={}):
        settings = get_model("settings").browse(1)
        return settings.currency_id.id

    def _get_currency_rates(self, context={}):
        settings = get_model("settings").browse(1)
        lines = []
        date = time.strftime("%Y-%m-%d")
        val = {
            "currency_id": settings.currency_id.id,
            "rate": settings.currency_id.get_rate(date, "sell") or 1
        }
        if context.get('action_name'):
            # default for new quotation create via quotation form
            lines.append(val)
        else:
            # When users create or copy quotation from other modules or methods, one2many field cannot be appended without action key
            # bacause it must be created in the database along with quotation itself.
            # If action key such as 'create', 'delete' is missing, the default line will not be created.
            # So, the action_key 'create' has to be appended into the list also.
            lines.append(("create", val))
        return lines

    _defaults = {
        "state": "draft",
        "date": lambda *a: time.strftime("%Y-%m-%d"),
        "number": _get_number,
        "currency_id": _get_currency,
        "tax_type": "tax_ex",
        "user_id": lambda self, context: get_active_user(),
        "uuid": lambda *a: str(uuid.uuid4()),
        "company_id": lambda *a: get_active_company(),
        "currency_rates": _get_currency_rates,
    }
    _constraints = ["check_fields"]
    _order = "date desc"

    def check_fields(self, ids, context={}):
        for obj in self.browse(ids):
            if context.get('is_draft'):
                continue
            dup = None
            if obj.state in ("waiting_approval", "approved"):
                if not obj.lines:
                    raise Exception("No lines in quotation")
            sequence_item = []
            for line in obj.lines:
                if line.sequence:
                    sequence_item.append(line.sequence)
            dup_sequence = set()
            for i in sequence_item:
                if sequence_item.count(i) >= 2:
                    dup_sequence.add(i)
                    dup = True
            if dup:
                raise Exception("Lines: Fields Item No. Duplicates : %s" %
                                (str(list(dup_sequence))))

    def create(self, vals, **kw):
        id = super().create(vals, **kw)
        self.function_store([id])
        if 'lines' in vals.keys():
            self.create_est_costs([id])
        return id

    def write(self, ids, vals, **kw):
        opport_ids = []
        for obj in self.browse(ids):
            if obj.opport_id:
                opport_ids.append(obj.opport_id.id)
        super().write(ids, vals, **kw)
        if opport_ids:
            get_model("sale.opportunity").function_store(opport_ids)
        if 'lines' in vals.keys():
            self.create_est_costs(ids)
        ctx = kw.get('context', {})
        self.function_store(ids, context=ctx)

    def function_store(self, ids, field_names=None, context={}):
        super().function_store(ids, field_names, context)
        opport_ids = []
        for obj in self.browse(ids):
            if obj.opport_id:
                opport_ids.append(obj.opport_id.id)
        if opport_ids:
            get_model("sale.opportunity").function_store(opport_ids)

    def get_amount(self, ids, context={}):
        res = {}
        for obj in self.browse(ids):
            vals = {}
            subtotal = 0
            tax = 0
            for line in obj.lines:
                if line.is_hidden:
                    continue
                if line.tax_id:
                    line_tax = get_model("account.tax.rate").compute_tax(
                        line.tax_id.id, line.amount, tax_type=obj.tax_type)
                else:
                    line_tax = 0
                tax += line_tax
                if obj.tax_type == "tax_in":
                    subtotal += (line.amount or 0) - line_tax
                else:
                    subtotal += line.amount or 0
            tax = get_model("currency").round(obj.currency_id.id, tax)
            vals["amount_subtotal"] = subtotal
            vals["amount_tax"] = tax
            vals["amount_total"] = subtotal + tax
            res[obj.id] = vals
        return res

    def get_qty_total(self, ids, context={}):
        res = {}
        for obj in self.browse(ids):
            qty = sum([line.qty for line in obj.lines])
            res[obj.id] = qty or 0
        return res

    def submit_for_approval(self, ids, context={}):
        for obj in self.browse(ids):
            if obj.state != "draft":
                raise Exception("Invalid state")
            obj.write({"state": "waiting_approval"})
        self.trigger(ids, "submit_for_approval")

    def approve(self, ids, context={}):
        for obj in self.browse(ids):
            if obj.state not in ("draft", "waiting_approval"):
                raise Exception("Invalid state")
            obj.write({"state": "approved"})

    def update_amounts(self, context):
        data = context["data"]
        data["amount_subtotal"] = 0
        data["amount_tax"] = 0
        tax_type = data["tax_type"]

        #===============>>>
        def _get_relative_currency_rate(currency_id):
            rate = None
            for r in data['currency_rates']:
                if r.get('currency_id') == currency_id:
                    rate = r.get('rate') or 0
                    break
            if rate is None:
                rate_from = get_model("currency").get_rate(
                    [currency_id], data['date']) or Decimal(1)
                rate_to = get_model("currency").get_rate(
                    [data['currency_id']], data['date']) or Decimal(1)
                rate = rate_from / rate_to
            return rate

        item_costs = {}
        for cost in data['est_costs']:
            if not cost:
                continue
            amt = cost['amount'] or 0
            if cost.get('currency_id'):
                print(cost.get("currency_id.id"), cost.get("currency_id"))
                rate = _get_relative_currency_rate(cost.get("currency_id"))
                amt = amt * rate
            comps = []
            if cost.get("sequence"):
                for comp in cost['sequence'].split("."):
                    comps.append(comp)
                    path = ".".join(comps)
                    k = (data['id'], path)
                    item_costs.setdefault(k, 0)
                    item_costs[k] += amt
        #<<<===============
        for line in data["lines"]:
            if not line:
                continue
            amt = (line.get("qty") or 0) * (line.get("unit_price") or 0)
            if line.get("discount"):
                disc = Decimal(amt) * Decimal(line["discount"]) / Decimal(100)
                amt -= disc
            if line.get("discount_amount"):
                amt -= line["discount_amount"]
            amt = Decimal(roundup(amt))
            line["amount"] = amt
            #===============>>>
            k = None
            if id in data:
                k = (data['id'], line.get("sequence", 0))
            else:
                k = (line.get("sequence", 0))
            cost = item_costs.get(k, 0)
            profit = amt - cost
            margin = profit * 100 / amt if amt else 0
            line["est_cost_amount"] = cost
            line["est_profit_amount"] = profit
            line["est_margin_percent"] = margin
            #<<<===============
        hide_parents = []
        for line in data["lines"]:
            if not line:
                continue
            if line.get("sequence") and line.get("hide_sub"):
                hide_parents.append(line["sequence"])
        is_hidden = {}
        hide_totals = {}
        for line in data["lines"]:
            if not line:
                continue
            if not line.get("sequence"):
                continue
            parent_seq = None
            for seq in hide_parents:
                if line["sequence"].startswith(seq + "."):
                    parent_seq = seq
                    break
            if parent_seq:
                is_hidden[line["sequence"]] = True
                hide_totals.setdefault(parent_seq, 0)
                hide_totals[parent_seq] += line["amount"]
        for line in data["lines"]:
            if not line:
                continue
            if line.get("sequence") and line.get("hide_sub"):
                line["amount"] = hide_totals.get(line["sequence"], 0)
                if line["qty"]:
                    line["unit_price"] = line["amount"] / line["qty"]
        for line in data["lines"]:
            if not line:
                continue
            if is_hidden.get(line.get("sequence")):
                continue
            tax_id = line.get("tax_id")
            if tax_id:
                tax = get_model("account.tax.rate").compute_tax(
                    tax_id, line["amount"], tax_type=tax_type)
                data["amount_tax"] += tax
            else:
                tax = 0
            if tax_type == "tax_in":
                data["amount_subtotal"] += Decimal(line["amount"] - tax)
            else:
                data["amount_subtotal"] += Decimal(line["amount"])
        data['amount_tax'] = get_model("currency").round(
            data['currency_id'], data['amount_tax'])
        data["amount_total"] = data["amount_subtotal"] + data["amount_tax"]
        return data

    def get_currency_rate(self, context={}):
        data = context['data']
        currency_id = data["currency_id"]
        currency_rate = 0
        for cr_rate in data['currency_rates']:
            if cr_rate['currency_id'] == currency_id:
                currency_rate = cr_rate['rate'] or 0
                break
        if not currency_rate:
            currency = get_model("currency").browse(currency_id)
            currency_rate = currency.get_rate(date=data['date'],
                                              rate_type="sell") or 1
            print('currency_rate ', currency_rate)
        return currency_rate

    def onchange_product(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if not prod_id:
            return {}
        prod = get_model("product").browse(prod_id)
        line["description"] = prod.description
        line["est_margin_percent_input"] = prod.gross_profit
        line["qty"] = prod.sale_unit_qty or prod.min_sale_qty or 1
        if prod.sale_uom_id or prod.uom_id is not None:
            line["uom_id"] = prod.sale_uom_id.id or prod.uom_id.id
        pricelist_id = data["price_list_id"]
        price = 0
        if pricelist_id:
            price = get_model("price.list").get_price(pricelist_id, prod.id, 1)
            line["unit_price"] = price
            line['discount'] = get_model("price.list").get_discount(
                pricelist_id, prod.id, 1)
        if not price:
            price = prod.sale_price
            settings = get_model("settings").browse(1)
            currency_rate = self.get_currency_rate(context)
            currency_id = data["currency_id"]
            price_cur = get_model("currency").convert(price,
                                                      settings.currency_id.id,
                                                      currency_id,
                                                      from_rate=1,
                                                      to_rate=currency_rate)
            line["unit_price"] = price_cur
        if prod.sale_tax_id is not None:
            line["tax_id"] = prod.sale_tax_id.id
        if prod.categ_id and prod.categ_id.sale_tax_id:
            line["tax_id"] = prod.categ_id.sale_tax_id.id
        contact_id = data.get('contact_id')
        if contact_id:
            contact = get_model("contact").browse(contact_id)
            if contact.tax_receivable_id:
                line["tax_id"] = contact.tax_receivable_id.id
        if data.get("tax_type", "") == "no_tax":
            line["tax_id"] = None
        data = self.update_amounts(context)
        return data

    def onchange_qty(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if not prod_id:
            return {}
        prod = get_model("product").browse(prod_id)
        pricelist_id = data["price_list_id"]
        qty = line.get("qty") or 0
        if line.get("unit_price") is None:
            if pricelist_id:
                line['unit_price'] = get_model("price.list").get_price(
                    pricelist_id, prod.id, qty)
            if not line.get('unit_price'):
                price = prod.sale_price
                settings = get_model("settings").browse(1)
                currency_id = data["currency_id"]
                currency_rate = self.get_currency_rate(context)
                price_cur = get_model("currency").convert(
                    price,
                    currency_id,
                    settings.currency_id.id,
                    from_rate=1,
                    to_rate=currency_rate)
                line["unit_price"] = price_cur
        data = self.update_amounts(context)
        return data

    def onchange_currency_rate(self, context={}):
        data = context['data']
        path = context['path']
        line = get_data_path(data, path, parent=True)
        currency = get_model("currency").browse(line['currency_id'])
        line['rate'] = currency.get_rate(date=data['date'],
                                         rate_type="sell") or 1
        data = self.update_line_currency(context)
        return data

    def onchange_currency(self, context):
        data = context['data']
        currency_id = data["currency_id"]
        currency = get_model("currency").browse(currency_id)
        rate = currency.get_rate(date=data['date'], rate_type="sell") or 1
        for crr in data['currency_rates']:
            crr.update({
                'currency_id': currency_id,
                'rate': rate,
            })
            break
        data = self.update_line_currency(context)
        return data

    def update_line_currency(self, context):
        settings = get_model("settings").browse(1)
        data = context['data']
        currency_id = data["currency_id"]
        currency_rate = self.get_currency_rate(context)
        pricelist_id = data["price_list_id"]
        for line in data['lines']:
            prod_id = line.get('product_id')
            if not prod_id:
                continue
            prod = get_model("product").browse(prod_id)
            qty = line.get('qty') or 0
            if pricelist_id:
                line['unit_price'] = get_model("price.list").get_price(
                    pricelist_id, prod.id, qty)
                continue
            price = prod.sale_price or 0
            settings = get_model("settings").browse(1)
            currency_id = data["currency_id"]
            price_cur = get_model("currency").convert(price,
                                                      currency_id,
                                                      settings.currency_id.id,
                                                      from_rate=1,
                                                      to_rate=currency_rate)
            line["unit_price"] = price_cur
        data = self.update_amounts(context)
        return data

    def onchange_contact(self, context):
        data = context["data"]
        contact_id = data.get("contact_id")
        if not contact_id:
            return {}
        contact = get_model("contact").browse(contact_id)
        data["payment_terms"] = contact.payment_terms
        data["price_list_id"] = contact.sale_price_list_id.id
        if contact.currency_id:
            data["currency_id"] = contact.currency_id.id
        else:
            settings = get_model("settings").browse(1)
            data["currency_id"] = settings.currency_id.id
        return data

    def onchange_uom(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if not prod_id:
            return {}
        prod = get_model("product").browse(prod_id)
        uom_id = line.get("uom_id")
        if not uom_id:
            return {}
        uom = get_model("uom").browse(uom_id)
        if prod.sale_price is not None:
            line[
                "unit_price"] = prod.sale_price * uom.ratio / prod.uom_id.ratio
        data = self.update_amounts(context)
        return data

    def copy(self, ids, context):
        obj = self.browse(ids)[0]
        vals = {
            "ref": obj.number,
            "contact_id": obj.contact_id.id,
            "currency_id": obj.currency_id.id,
            "tax_type": obj.tax_type,
            "payment_terms": obj.payment_terms,
            "other_info": obj.other_info,
            "exp_date": obj.exp_date,
            "opport_id": obj.opport_id.id,
            "lines": [],
        }
        for line in obj.lines:
            line_vals = {
                "product_id": line.product_id.id,
                "description": line.description,
                "qty": line.qty,
                "uom_id": line.uom_id.id,
                "unit_price": line.unit_price,
                "discount": line.discount,
                "discount_amount": line.discount_amount,
                "tax_id": line.tax_id.id,
                'amount': line.amount,
                'sequence': line.sequence,
            }
            vals["lines"].append(("create", line_vals))
        new_id = self.create(vals, context=context)
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "quot",
                "mode": "form",
                "active_id": new_id,
            },
            "flash":
            "Quotation %s copied from %s" % (new_obj.number, obj.number),
        }

    def revise(self, ids, context):
        obj = self.browse(ids)[0]
        res = self.copy(ids, context)
        obj.write({"state": "revised"})
        return res

    def copy_to_sale_order(self, ids, context):
        id = ids[0]
        obj = self.browse(id)
        sale_vals = {
            "quot_id": obj.id,
            "contact_id": obj.contact_id.id,
            "currency_id": obj.currency_id.id,
            "tax_type": obj.tax_type,
            "ref": obj.ref,
            "lines": [],
            "user_id": obj.user_id.id,
            "other_info": obj.other_info,
            "payment_terms": obj.payment_terms,
            "price_list_id": obj.price_list_id.id,
            "job_template_id": obj.job_template_id.id,
            "est_costs": [],
            "currency_rates": [],
            "related_id": "sale.quot,%s" % obj.id,
        }
        for line in obj.lines:
            if not line.qty:
                continue
            prod = line.product_id
            line_vals = {
                "sequence": line.sequence,
                "product_id": prod.id,
                "description": line.description,
                "qty": line.qty,
                "uom_id": line.uom_id and line.uom_id.id or None,
                "unit_price": line.unit_price if not line.is_hidden else 0,
                "discount": line.discount if not line.is_hidden else 0,
                "discount_amount":
                line.discount_amount if not line.is_hidden else 0,
                "tax_id": line.tax_id.id if not line.is_hidden else None,
                "location_id": prod.location_id.id if prod else None,
            }
            if prod.locations:
                line_vals["location_id"] = prod.locations[0].location_id.id
                for loc in prod.locations:
                    if loc.stock_qty:
                        line_vals['location_id'] = loc.location_id.id
                        break
            sale_vals["lines"].append(("create", line_vals))
        for r in obj.currency_rates:
            rate_vals = {
                "currency_id": r.currency_id.id,
                "rate": r.rate,
            }
            sale_vals["currency_rates"].append(("create", rate_vals))
        sale_id = get_model("sale.order").create(sale_vals, context=context)
        sale = get_model("sale.order").browse(sale_id)
        return {
            "next": {
                "name": "sale",
                "mode": "form",
                "active_id": sale_id,
            },
            "flash":
            "Sale order %s created from quotation %s" %
            (sale.number, obj.number)
        }

    def do_won(self, ids, context={}):
        for obj in self.browse(ids):
            assert obj.state == "approved"
            obj.write({"state": "won"})

    def do_lost(self, ids, context={}):
        for obj in self.browse(ids):
            assert obj.state == "approved"
            obj.write({"state": "lost"})

    def do_reopen(self, ids, context={}):
        for obj in self.browse(ids):
            assert obj.state in ("won", "lost")
            obj.write({"state": "approved"})

    def get_state(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            state = obj.state
            if state == "approved":
                found = False
                for sale in obj.sales:
                    if sale.state in ("confirmed", "done"):
                        found = True
                        break
                if found:
                    state = "won"
            vals[obj.id] = state
        return vals

    def view_link(self, ids, context={}):
        obj = self.browse(ids)[0]
        uuid = obj.uuid
        dbname = get_active_db()
        return {
            "next": {
                "type": "url",
                "url": "/view_quot?dbname=%s&uuid=%s" % (dbname, uuid),
            }
        }

    def get_template_quot_form(self, ids, context={}):
        obj = self.browse(ids)[0]
        has_discount = False
        for line in obj.lines:
            if line.discount:
                has_discount = True
        if has_discount:
            return "quot_form_disc"
        else:
            return "quot_form"

    def to_draft(self, ids, context={}):
        obj = self.browse(ids)[0]
        context['is_draft'] = True
        obj.write({"state": "draft"}, context=context)

    def get_amount_total_words(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            amount_total_words = utils.num2word(obj.amount_total)
            vals[obj.id] = amount_total_words
            return vals

    def onchange_sequence(self, context={}):
        data = context["data"]
        seq_id = data["sequence_id"]
        context['date'] = data['date']
        if not seq_id:
            seq_id = get_model("sequence").find_sequence(type="sale_quot")
        while 1:
            num = get_model("sequence").get_next_number(seq_id,
                                                        context=context)
            res = self.search([["number", "=", num]])
            if not res:
                break
            get_model("sequence").increment_number(seq_id, context=context)
        data["number"] = num
        return data

    def onchange_cost_product(self, context):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        prod_id = line.get("product_id")
        if prod_id:
            prod = get_model("product").browse(prod_id)
            line["description"] = prod.name
            line["list_price"] = prod.purchase_price
            line["purchase_price"] = prod.purchase_price
            line["landed_cost"] = prod.landed_cost
            line["qty"] = 1
            line["uom_id"] = prod.uom_id.id
            line["currency_id"] = prod.purchase_currency_id.id
            line["purchase_duty_percent"] = prod.purchase_duty_percent
            line["purchase_ship_percent"] = prod.purchase_ship_percent
            line["landed_cost"] = prod.landed_cost
            line["amount"] = line['qty'] * line['landed_cost'] or 0
            if prod.suppliers:
                line["supplier_id"] = prod.suppliers[0].supplier_id.id
        return data

    def get_est_profit(self, ids, context={}):
        vals = {}
        for obj in self.browse(ids):
            cost = 0
            cost_conv = 0
            settings = get_model('settings').browse(1)
            currency_id = settings.currency_id.id
            cur_rate = self.get_relative_currency_rate(ids, obj.currency_id.id)
            amount = obj.amount_subtotal * cur_rate
            amount_conv = obj.amount_subtotal
            if currency_id == obj.currency_id.id:
                amount_conv = 0
            for line_cost in obj.est_costs:
                cost += line_cost.amount or 0
                cost_conv += line_cost.amount_conv or 0
            profit = (amount or 0) - cost
            margin = (profit / amount) * 100 if amount else None
            profit_conv = (amount_conv or 0) - cost_conv
            margin_conv = (profit_conv /
                           amount_conv) * 100 if amount_conv else None
            vals[obj.id] = {
                "est_cost_amount": cost,
                "est_profit_amount": profit,
                "est_margin_percent": margin,
                "est_cost_amount_conv": cost_conv if amount_conv else 0,
                "est_profit_amount_conv": profit_conv if amount_conv else 0,
                "est_margin_percent_conv": margin_conv if amount_conv else 0,
                "est_sale_amount_conv": amount_conv,
                "est_sale_amount": amount,
            }
        return vals

    def create_est_costs(self, ids, context={}):
        obj = self.browse(ids[0])
        del_ids = []
        for cost in obj.est_costs:
            if cost.product_id:
                del_ids.append(cost.id)
        get_model("quot.cost").delete(del_ids)
        #obj.write({"est_costs":[("delete_all",)]})
        line_sequence = 1
        settings = get_model("settings").browse(1)
        for line in obj.lines:
            prod = line.product_id
            cur_line_sequence = line_sequence
            landed_cost = prod.landed_cost
            if not prod:
                continue
            if not prod.purchase_price and prod.type != "service":
                continue
            if not prod.cost_price and prod.type == "service":
                continue
            if "bundle" == prod.type:
                continue
            # update line seqence
            if not line.sequence:
                line.write({"sequence": cur_line_sequence})
                line_sequence += 1
            else:
                line_sequence = round(Decimal(line.sequence)) + Decimal(1)
            # compute cost if product is service
            if prod.type == "service":
                if prod.uom_id.type == 'time':  #day
                    landed_cost = prod.cost_price or 0
                elif prod.uom_id.type == 'unit':  #job
                    landed_cost = (prod.sale_price or 0) * (prod.cost_price
                                                            or 0)  #percentage
            #elif prod.type == "stock" and prod.supply_method == 'production':
            #landed_cost = prod.cost_price or 0
            # update landed cost
            per_duty = ((Decimal('1.0') * prod.purchase_duty_percent
                         if prod.purchase_duty_percent else Decimal('0')) /
                        Decimal('100.0'))
            per_ship = ((Decimal('1.0') * prod.purchase_ship_percent
                         if prod.purchase_ship_percent else Decimal('0')) /
                        Decimal('100.0'))
            landed_cost = prod.purchase_price + (
                prod.purchase_price * per_ship) + (prod.purchase_price *
                                                   per_duty)
            vals = {
                "quot_id":
                obj.id,
                "sequence":
                (line.sequence if not line.is_hidden else line.parent_sequence)
                if line.sequence else cur_line_sequence,
                "product_id":
                prod.id,
                "description":
                prod.name,
                "supplier_id":
                prod.suppliers[0].supplier_id.id if prod.suppliers else None,
                "list_price":
                prod.purchase_price,
                "purchase_price":
                prod.purchase_price,
                #"landed_cost": prod.cost_price if prod.type == "service" else prod.landed_cost,
                "landed_cost":
                landed_cost,
                "purchase_duty_percent":
                prod.purchase_duty_percent,
                "purchase_ship_percent":
                prod.purchase_ship_percent,
                "qty":
                line.qty,
                "currency_id":
                prod.purchase_currency_id.id or settings.currency_id.id,
            }
            get_model("quot.cost").create(vals)

    def merge_quotations(self, ids, context={}):
        if len(ids) < 2:
            raise Exception("Can not merge less than two quotations")
        cur_rate = self.get_relative_currency_rate(ids, obj.currency_id.id)
        contact_ids = []
        currency_ids = []
        tax_types = []
        for obj in self.browse(ids):
            contact_ids.append(obj.contact_id.id)
            currency_ids.append(obj.currency_id.id)
            tax_types.append(obj.tax_type)
        contact_ids = list(set(contact_ids))
        currency_ids = list(set(currency_ids))
        tax_types = list(set(tax_types))
        if len(contact_ids) > 1:
            raise Exception("Quotation customers have to be the same")
        if len(currency_ids) > 1:
            raise Exception("Quotation currencies have to be the same")
        if len(tax_types) > 1:
            raise Exception("Quotation tax types have to be the same")
        vals = {
            "contact_id": contact_ids[0],
            "currency_id": currency_ids[0],
            "tax_type": tax_types[0],
            "lines": [],
            "est_costs": [],
        }
        seq = 0
        refs = []
        for obj in sorted(self.browse(ids), key=lambda obj: obj.number):
            refs.append(obj.number)
            seq_map = {}
            for line in obj.lines:
                seq += 1
                seq_map[line.sequence] = seq
                qty = line.qty or 0
                unit_price = line.unit_price or 0
                amt = qty * unit_price
                disc = amt * (line.discount or 0) / Decimal(100)
                line_vals = {
                    "sequence": seq,
                    "product_id": line.product_id.id,
                    "description": line.description,
                    "qty": qty,
                    "uom_id": line.uom_id.id,
                    "unit_price": unit_price,
                    "discount": disc,
                    "amount": amt,
                    "tax_id": line.tax_id.id,
                }
                vals["lines"].append(("create", line_vals))
            for cost in obj.est_costs:
                cost_vals = {
                    "sequence": seq_map.get(cost.sequence),
                    "product_id": cost.product_id.id,
                    "description": cost.description,
                    "supplier_id": cost.supplier_id.id,
                    "list_price": cost.list_price,
                    "purchase_price": cost.purchase_price,
                    "landed_cost": cost.landed_cost,
                    "qty": cost.qty,
                    "currency_id": cost.currency_id.id,
                }
                vals["est_costs"].append(("create", cost_vals))
        vals['ref'] = ', '.join([ref for ref in refs])
        new_id = self.create(vals, context=context)
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "quot",
                "mode": "form",
                "active_id": new_id,
            },
            "flash": "Quotations merged",
        }

    def onchange_est_margin(self, context={}):
        data = context["data"]
        path = context["path"]
        line = get_data_path(data, path, parent=True)
        margin = line["est_margin_percent_input"]
        amt = line["est_cost_amount"] / (1 - margin / Decimal(100))
        price = round(amt / line["qty"])
        line["unit_price"] = price
        self.update_amounts(context)
        return data

    def get_relative_currency_rate(self, ids, currency_id):
        obj = self.browse(ids[0])
        rate = None
        for r in obj.currency_rates:
            if r.currency_id.id == currency_id:
                rate = r.rate
                break
        if rate is None:
            rate_from = get_model("currency").get_rate([currency_id],
                                                       obj.date) or Decimal(1)
            rate_to = obj.currency_id.get_rate(obj.date) or Decimal(1)
            rate = rate_from / rate_to
        return rate

    def update_cost_amount(self, context={}):
        settings = get_model('settings').browse(1)
        data = context['data']
        path = context['path']
        line = get_data_path(data, path, parent=True)

        pur_price = round(Decimal(line['purchase_price'] or 0), 2)
        purchase_ship_percent = round(
            Decimal(line['purchase_ship_percent'] or 0), 2)
        purchase_duty_percent = round(
            Decimal(line['purchase_duty_percent'] or 0), 2)
        qty = round(Decimal(line['qty'] or 0), 2)
        amount = Decimal(0)

        landed_cost = round(
            pur_price + (pur_price * (purchase_ship_percent / 100)) +
            (pur_price * (purchase_duty_percent / 100)), 2)
        amount = round((qty) * (landed_cost), 2)

        line['landed_cost'] = landed_cost
        prod = get_model('product').browse(line.get('product_id'))
        if prod.purchase_currency_id:
            if prod.purchase_currency_id.id == settings.currency_id.id:
                if data["currency_id"] == settings.currency_id.id:
                    line['amount'] = amount
                    line['amount_conv'] = amount
                else:
                    cost_conv = get_model("currency").convert(
                        amount,
                        settings.currency_id.id,
                        data['currency_id'],
                        date=data['date'])
                    line['amount'] = amount
                    line['amount_conv'] = cost_conv
            else:
                cost = get_model("currency").convert(
                    amount,
                    prod.purchase_currency_id.id,
                    settings.currency_id.id,
                    date=data['date'])
                cost_conv = get_model("currency").convert(
                    cost,
                    settings.currency_id.id,
                    data['currency_id'],
                    date=data['date'])
                line['amount'] = cost
                line['amount_conv'] = cost_conv
        else:
            if data["currency_id"] != settings.currency_id.id:
                cost = get_model("currency").convert(amount,
                                                     data['currency_id'],
                                                     settings.currency_id.id,
                                                     date=data['date'])
                cost_conv = get_model("currency").convert(
                    cost,
                    settings.currency_id.id,
                    data['currency_id'],
                    date=data['date'])
                line['amount'] = cost
                line['amount_conv'] = cost_conv
            else:
                line['amount'] = amount
                line['amount_conv'] = amount
        return data

    def onchange_tax_type(self, context={}):
        data = context['data']
        if data['tax_type'] == 'no_tax':
            for line in data['lines']:
                line['tax_id'] = None
        else:
            for line in data['lines']:
                if 'product_id' not in line:
                    continue
                product_id = line['product_id']
                if not product_id:
                    continue
                if 'tax_id' not in line or line['tax_id']:
                    continue
                product = get_model("product").browse(product_id)
                if product.sale_tax_id:
                    line["tax_id"] = product.sale_tax_id.id
        data = self.update_amounts(context)
        return data

    def onchange_date(self, context={}):
        data = self.onchange_sequence(context)
        data = self.onchange_currency(context)
        return data
Example #8
0
class Move(Model):
    _name = "account.move"
    _string = "Journal Entry"
    _name_field = "number"
    _multi_company = True
    _audit_log = True
    _key = ["company_id", "number"]
    _fields = {
        "journal_id":
        fields.Many2One("account.journal",
                        "Journal",
                        required=True,
                        search=True),
        "narration":
        fields.Text("Narration", required=True, search=True),
        "date":
        fields.Date("Document Date", required=True, search=True, index=True),
        "date_posted":
        fields.Date("Posted Date", search=True, index=True),
        "state":
        fields.Selection(
            [["draft", "Draft"], ["posted", "Posted"], ["voided", "Voided"]],
            "Status",
            required=True,
            search=True),
        "lines":
        fields.One2Many("account.move.line", "move_id", "Lines"),
        "total_debit":
        fields.Decimal("Total Debit",
                       function="get_total",
                       function_multi=True),
        "total_credit":
        fields.Decimal("Total Credit",
                       function="get_total",
                       function_multi=True),
        "type":
        fields.Selection([["auto", "auto"], ["manual", "Manual"]], "Type"),
        "ref":
        fields.Char("Reference", search=True),
        "number":
        fields.Char("Number", search=True, required=True),
        "default_line_desc":
        fields.Boolean("Default narration to line description"),
        "comments":
        fields.One2Many("message", "related_id", "Comments"),
        "related_id":
        fields.Reference(
            [["account.invoice", "Invoice"], ["account.payment", "Payment"],
             ["account.transfer", "Transfer"], ["hr.expense", "Expense Claim"],
             ["service.contract", "Service Contract"], ["pawn.loan", "Loan"],
             ["landed.cost", "Landed Cost"], [
                 "stock.picking", "Stock Picking"
             ], ['account.advance', 'Advance'],
             ['account.advance.clear', 'Advance Clear']], "Related To"),
        "company_id":
        fields.Many2One("company", "Company"),
        "track_entries":
        fields.One2Many("account.track.entry", "move_id", "Tracking Entries"),
        "difference":
        fields.Float("Difference",
                     function="get_difference",
                     function_multi=True),
        "verified":
        fields.Boolean("Verified", search=True),
        "documents":
        fields.One2Many("document", "related_id", "Documents"),
    }

    def _get_journal(self, context={}):
        settings = get_model("settings").browse(1)
        return settings.general_journal_id.id

    def _get_number(self, context={}):
        journal_id = context.get("journal_id")
        if not journal_id:
            settings = get_model("settings").browse(1)
            journal_id = settings.general_journal_id.id
        if not journal_id:
            return
        journal = get_model("account.journal").browse(journal_id)
        seq_id = journal.sequence_id.id
        if not seq_id:
            return
        while 1:
            num = get_model("sequence").get_next_number(seq_id,
                                                        context=context)
            res = self.search([["number", "=", num]])
            if not res:
                return num
            get_model("sequence").increment_number(seq_id, context=context)

    _defaults = {
        "state": "draft",
        "default_line_desc": True,
        "type": "auto",
        "date": lambda *a: time.strftime("%Y-%m-%d"),
        "journal_id": _get_journal,
        "number": _get_number,
        "company_id": lambda *a: get_active_company(),
    }
    _order = "date desc,id desc"

    def get_difference(self, ids, context):
        vals = {}
        for obj in self.browse(ids):
            vals[obj.id] = {
                "difference": obj.total_debit - obj.total_credit,
            }
        return vals

    def create(self, vals, **kw):
        t0 = time.time()
        new_id = super().create(vals, **kw)
        t01 = time.time()
        dt01 = (t01 - t0) * 1000
        print("account_move.dt01", dt01)
        obj = self.browse([new_id])[0]
        line_ids = []
        rec_ids = []
        for line in obj.lines:
            line_ids.append(line.id)
            if line.reconcile_id:
                rec_ids.append(line.reconcile_id.id)
        get_model("account.move.line").function_store(line_ids)
        if rec_ids:
            get_model("account.reconcile").function_store(rec_ids)
        get_model("field.cache").clear_cache(model="account.account")
        t1 = time.time()
        dt = (t1 - t0) * 1000
        print("account_move.create <<< %d ms" % dt)
        return new_id

    def write(self, ids, vals, **kw):
        super().write(ids, vals, **kw)
        line_ids = []
        rec_ids = []
        check_tax_no = None
        for obj in self.browse(ids):
            for line in obj.lines:
                line_ids.append(line.id)
                if line.reconcile_id:
                    rec_ids.append(line.reconcile_id.id)
                if line.tax_no:
                    check_tax_no = get_model("account.move.line").search(
                        [["tax_no", "=", line.tax_no],
                         ["move_id", "!=", obj.id]])
                if check_tax_no:
                    raise Exception("%s : Duplicate Tax No." % (line.tax_no))
        get_model("account.move.line").function_store(line_ids)
        if rec_ids:
            get_model("account.reconcile").function_store(rec_ids)
        get_model("field.cache").clear_cache(model="account.account")

    def delete(self, ids, **kw):
        rec_ids = []
        prefix = ''
        q = "SELECT id,name,prefix,type from sequence where type='fixed_asset' order by type"
        db = database.get_connection()
        res = db.get(q)
        for st in res.prefix:
            if st == '-' or st == '%':
                break
            prefix += st
        move = self.browse(ids)
        for obj in self.browse(ids):
            if obj.state == "posted":
                raise Exception("Can not deleted posted journal entry")
            if obj.related_id and prefix in obj.related_id.number:
                raise Exception("This JV still have related")
            for line in obj.lines:
                if line.reconcile_id:
                    rec_ids.append(line.reconcile_id.id)
        super().delete(ids, **kw)
        if rec_ids:
            get_model("account.reconcile").function_store(rec_ids)
        get_model("field.cache").clear_cache(model="account.account")

    def post(self, ids, context={}):
        settings = get_model("settings").browse(1)
        for obj in self.browse(ids):
            if settings.lock_date:
                assert obj.date >= settings.lock_date, "Accounting transaction is before lock date"
            if obj.state != "draft":
                raise Exception("Journal entry is not draft")
            total_debit = 0
            total_credit = 0
            for line in obj.lines:
                acc = line.account_id
                if not acc.active:
                    raise Exception(
                        "Can not post this JV becuase [%s]%s was archeived" %
                        (acc.code, acc.name))
                if acc.type == "view":
                    raise Exception(
                        "Can not post to 'view' account ([%s] %s)" %
                        (acc.code, acc.name))
                if acc.company_id.id != obj.company_id.id:
                    raise Exception(
                        "Wrong company for account %s in journal entry %s (account company: %s, journal entry company %s)("
                        % (acc.code, obj.number, acc.company_id.code,
                           obj.company_id.code))
                if acc.require_contact and not line.contact_id:
                    raise Exception("Missing contact for account %s" %
                                    acc.code)
                if acc.require_tax_no and not line.tax_no:
                    raise Exception("Missing tax number for account %s" %
                                    acc.code)
                if acc.require_track and not line.track_id:
                    raise Exception(
                        "Missing tracking category for account %s" % acc.code)
                if acc.require_track2 and not line.track2_id:
                    raise Exception(
                        "Missing secondary tracking category for account %s" %
                        acc.code)
                if line.debit < 0:
                    raise Exception("Debit amount is negative (%s)" %
                                    line.debit)
                if line.credit < 0:
                    raise Exception("Credit amount is negative (%s)" %
                                    line.credit)
                if line.debit > 0 and line.credit > 0:
                    raise Exception(
                        "Debit and credit amounts can not be both non-zero (account: %s, debit: %s, credit: %s)"
                        % (line.account_id.name_get()[0][1], line.debit,
                           line.credit))
                total_debit += line.debit
                total_credit += line.credit
                if line.tax_comp_id and not line.tax_date:
                    line.write({"tax_date": line.move_id.date})
                if acc.currency_id.id != settings.currency_id.id and not line.amount_cur and (
                        line.debit - line.credit) != 0:
                    raise Exception(
                        "Missing currency amount for account %s" %
                        line.account_id.name_get()[0][1])  ## issue to STD
                if line.amount_cur and line.credit == 0 and line.debit == 0:
                    raise Exception(
                        "Missing credit/debit amount for account %s" %
                        line.account_id.name_get()[0][1])
            if abs(total_debit - total_credit) != 0:
                print("NOT BALANCED total_debit=%s total_credit=%s" %
                      (total_debit, total_credit))
                for line in obj.lines:
                    print("  ACC: [%s] %s DR: %s CR: %s" %
                          (line.account_id.code, line.account_id.name,
                           line.debit, line.credit))
                raise Exception(
                    "Journal entry is not balanced (debit=%s, credit=%s)" %
                    (total_debit, total_credit))
            obj.write({"state": "posted"})
            if not obj.date_posted:
                date_posted = time.strftime("%Y-%m-%d")
                obj.write({"date_posted": date_posted})
            obj.create_track_entries()

            #order line by debit and credit
            debit_lines = []
            credit_lines = []
            for line in obj.lines:
                line_vals = (line, line.account_id.code_number)
                if line.debit:
                    debit_lines.append(line_vals)
                else:
                    credit_lines.append(line_vals)

            for index, lines in enumerate([debit_lines, credit_lines]):
                for index2, line_vals in enumerate(
                        sorted(lines, key=lambda a: a[1])):
                    line, acc_code = line_vals
                    seq = int(''.join([str(index + 1), str(index2 + 1)]))
                    line.write({'sequence': seq})

        if not context.get("no_reconcile"):
            bank_ids = []
            for obj in self.browse(ids):
                for line in obj.lines:
                    acc = line.account_id
                    if acc.type in ("bank", "cash", "cheque"):
                        bank_ids.append(acc.id)
            if bank_ids:
                bank_ids = list(set(bank_ids))
                get_model("account.account").auto_bank_reconcile(bank_ids)
        get_model("account.balance").update_balances()

        active_id = ids[-1]
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": active_id,
            },
            "flash": "Journal entry #%d is posted" % active_id
        }

    def create_track_entries(self, ids, context={}):
        obj = self.browse(ids[0])
        settings = get_model("settings").browse(1)
        for line in obj.lines:
            if line.track_id:
                amt = line.credit - line.debit
                if line.track_id.currency_id:
                    amt = get_model("currency").convert(
                        amt,
                        settings.currency_id.id,
                        line.track_id.currency_id.id,
                        date=obj.date)
                vals = {
                    "date":
                    obj.date,
                    "track_id":
                    line.track_id.id,
                    "amount":
                    amt,
                    "description":
                    line.description,
                    "move_id":
                    obj.id,
                    "related_id":
                    "%s,%s" % (line.move_id.related_id._model,
                               line.move_id.related_id.id),
                }
                get_model("account.track.entry").create(vals)
            if line.track2_id:
                amt = line.credit - line.debit
                if line.track2_id.currency_id:
                    amt = get_model("currency").convert(
                        amt,
                        settings.currency_id.id,
                        line.track2_id.currency_id.id,
                        date=obj.date)
                vals = {
                    "date":
                    obj.date,
                    "track_id":
                    line.track2_id.id,
                    "amount":
                    amt,
                    "description":
                    line.description,
                    "move_id":
                    obj.id,
                    "related_id":
                    "%s,%s" % (line.move_id.related_id._model,
                               line.move_id.related_id.id),
                }
                get_model("account.track.entry").create(vals)

    def void(self, ids, context={}):
        obj = self.browse(ids[0])
        settings = get_model("settings").browse(1)
        if settings.lock_date:
            if obj.date < settings.lock_date:
                raise Exception("Accounting transaction is before lock date")
        obj.lines.unreconcile()
        obj.write({"state": "voided"})
        obj.delete_track_entries()
        get_model("field.cache").clear_cache(model="account.account")
        get_model("account.balance").update_balances()

    def related_void(self, ids, context={}):
        obj = self.browse(ids[0])
        settings = get_model("settings").browse(1)
        if obj.related_id:
            if obj.related_id._model == "account.payment":
                if obj.related_id.type == "out":
                    raise Exception(
                        "Can not be void. because it has a Supplier Payments related id = %s"
                        % (obj.related_id.number))
                else:
                    raise Exception(
                        "Can not be void. because it has a Customer Payments related id = %s"
                        % (obj.related_id.number))
            elif obj.related_id._model == "account.invoice":
                res = get_model("account.payment.line").search_browse(
                    [["invoice_id", "=", obj.related_id.id]])
                if res:
                    paymeny_id = []
                    for s in res:
                        paymeny_id.append(s.payment_id.number)
                    raise Exception(
                        "Can not be void. because it has a payment related id = %s"
                        % (paymeny_id))
                if obj.related_id.type == "out":
                    raise Exception(
                        "Can not be void. because it has a Accounts Payable related id = %s"
                        % (obj.related_id.number))
                else:
                    raise Exception(
                        "Can not be void. because it has a Accounts Receivable related id = %s"
                        % (obj.related_id.number))
            elif obj.related_id._model == "account.advance":
                raise Exception(
                    "Can not be void. because it has a Advance Payment related id = %s"
                    % (obj.related_id.number))
            elif obj.related_id._model == "account.advance.clear":
                raise Exception(
                    "Can not be void. because it has a Advance Clearing related id = %s"
                    % (obj.related_id.number))
            elif obj.related_id._model == "account.cheque.move":
                if obj.related_id.type == "RP":
                    raise Exception(
                        "Can not be void. because it has a Cheque Honor Receipt related id = %s"
                        % (obj.related_id.number))
                elif obj.related_id.type == "PP":
                    raise Exception(
                        "Can not be void. because it has a Cheque Honor Payment related id = %s"
                        % (obj.related_id.number))
            elif obj.related_id._model == "hr.expense":
                raise Exception(
                    "Can not be void. because it has a account_expense related id = %s"
                    % (obj.related_id.number))
            elif obj.related_id._model == "stock.picking":
                raise Exception(
                    "Can not be void. because it has a stock_picking related id = %s"
                    % (obj.related_id.number))
            elif obj.related_id._model == "account.transfer":
                raise Exception(
                    "Can not be void. because it has a Bank Transfer related id = %s"
                    % (obj.related_id.number))

        if settings.lock_date:
            if obj.date < settings.lock_date:
                raise Exception("Accounting transaction is before lock date")
        obj.lines.unreconcile()
        obj.write({"state": "voided"})
        obj.delete_track_entries()
        get_model("field.cache").clear_cache(model="account.account")
        get_model("account.balance").update_balances()

    def delete_track_entries(self, ids, context={}):
        obj = self.browse(ids[0])
        obj.track_entries.delete()

    def get_total(self, ids, context):
        vals = {}
        for obj in self.browse(ids):
            total_debit = 0
            total_credit = 0
            for line in obj.lines:
                total_debit += line.debit
                total_credit += line.credit
            vals[obj.id] = {
                "total_debit": total_debit,
                "total_credit": total_credit,
            }
        return vals

    def update_amounts(self, context):
        data = context["data"]
        data["total_debit"] = 0
        data["total_credit"] = 0
        for line in data["lines"]:
            if not line:
                continue
            debit = line.get("debit") or 0
            credit = line.get("credit") or 0
            data["total_debit"] += debit
            data["total_credit"] += credit
            if line.get("debit") is not None and line.get("credit") is None:
                line["credit"] = 0
            if line.get("credit") is not None and line.get("debit") is None:
                line["debit"] = 0
        data["difference"] = data["total_debit"] - data["total_credit"]
        return data

    def get_line_desc(self, context):
        data = context["data"]
        path = context["path"]
        if not data.get("default_line_desc"):
            return
        if not get_data_path(data, path):
            set_data_path(data, path, data.get("narration"))
        return data

    def view_journal(self, ids, context={}):
        res = self.read(ids, ["related_id"])[0]["related_id"]
        rel = res and res[0] or None
        next = None
        if rel:
            model, model_id = rel.split(",")
            if model == "account.invoice":
                next = {
                    "name": "view_invoice",
                    "active_id": model_id,
                }
            elif model == "account.payment":
                next = {
                    "name": "payment",
                    "mode": "form",
                    "active_id": model_id,
                }
            elif model == "account.transfer":
                next = {
                    "name": "bank_transfer",
                    "mode": "form",
                    "active_id": model_id,
                }
            elif model == "account.claim":
                next = {
                    "name": "account_claim_edit",
                    "active_id": model_id,
                }
        if not next:
            next = {
                "name": "journal_entry",
                "mode": "form",
                "active_id": ids[0],
            }
        return {"next": next}

    def copy(self, ids, context={}):
        obj = self.browse(ids)[0]
        vals = {
            "journal_id": obj.journal_id.id,
            "ref": obj.ref,
            "default_line_desc": obj.default_line_desc,
            "narration": obj.narration,
            "lines": [],
        }
        for line in obj.lines:
            if not line.account_id.active:
                raise Exception(
                    'Can not copy this because account : [%s]%s was archived' %
                    (line.account_id.code, line.account_id.name))
            line_vals = {
                "description": line.description,
                "account_id": line.account_id.id,
                "debit": line.debit,
                "credit": line.credit,
                "tax_comp_id": line.tax_comp_id.id,
                "tax_base": line.tax_base,
                "contact_id": line.contact_id.id,
                "track_id": line.track_id.id,
                "track2_id": line.track2_id.id,
            }
            vals["lines"].append(("create", line_vals))
        new_id = self.create(vals, context={"journal_id": obj.journal_id.id})
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": new_id,
            },
            "flash":
            "Journal entry %s copied to %s" % (obj.number, new_obj.number),
        }

    def to_draft(self, ids, context={}):
        settings = get_model("settings").browse(1)
        obj = self.browse(ids)[0]
        if settings.lock_date:
            assert obj.date >= settings.lock_date, "Accounting transaction is before lock date"
        for line in obj.lines:
            line.unreconcile()
        obj.write({"state": "draft"})
        obj.delete_track_entries()
        get_model("account.balance").update_balances()
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": obj.id,
            },
            "flash": "Journal entry #%d set to draft" % obj.id,
        }

    def onchange_journal(self, context={}):
        data = context["data"]
        journal_id = data["journal_id"]
        date = data["date"]
        number = self._get_number(context={
            "journal_id": journal_id,
            "date": date
        })
        data["number"] = number
        return data

    def onchange_date(self, context={}):
        data = context["data"]
        journal_id = data["journal_id"]
        date = data["date"]
        number = self._get_number(context={
            "journal_id": journal_id,
            "date": date
        })
        data["number"] = number
        return data

    def get_data(self, ids, context={}):
        company_id = get_active_company()
        comp = get_model("company").browse(company_id)
        settings = get_model('settings').browse(1)
        pages = []
        for obj in self.browse(ids):
            lines = []
            for line in obj.lines:
                lines.append({
                    'description': line.description,
                    'account_code': line.account_id.code,
                    'account_name': line.account_id.name,
                    'debit': line.debit,
                    'credit': line.credit,
                    'tax_comp': line.tax_comp_id.name,
                    'tax_base': line.tax_base,
                    'track': line.track_id.name,
                    'contact': line.contact_id.name,
                })
            data = {
                "comp_name": comp.name,
                "number": obj.number,
                "date": obj.date,
                "journal": obj.journal_id.name,
                "narration": obj.narration,
                "lines": lines,
                "total_debit": obj.total_debit,
                "total_credit": obj.total_credit,
            }
            if settings.logo:
                data['logo'] = get_file_path(settings.logo)
            pages.append(data)
        if pages:
            pages[-1]["is_last_page"] = True
        return {
            "pages": pages,
            "logo":
            get_file_path(settings.logo),  # XXX: remove when render_odt fixed
        }

    def get_report_data(self, ids=None, context={}):
        if ids is not None:  # for new templates
            return super().get_report_data(ids, context=context)
        ids = context["ids"]
        return self.get_data(ids, context)

    def reverse(self, ids, context={}):
        settings = get_model("settings").browse(1)
        obj = self.browse(ids)[0]
        rel = obj.related_id
        vals = {
            "journal_id": obj.journal_id.id,
            "ref": obj.ref,
            "default_line_desc": obj.default_line_desc,
            "narration": obj.narration,
            "lines": [],
        }
        if rel:
            vals['related_id'] = "%s,%s" % (rel._model, rel.id)
        for line in obj.lines:
            line_vals = {
                "description": line.description,
                "account_id": line.account_id.id,
                "debit": line.credit,
                "credit": line.debit,
                "tax_comp_id": line.tax_comp_id.id,
                "tax_base": line.tax_base,
                "contact_id": line.contact_id.id,
                "track_id": line.track_id.id,
                "track2_id": line.track2_id.id,
            }
            acc = line.account_id
            if acc.currency_id.id != settings.currency_id.id:
                amt = line.amount_cur or 0
                sign = amt < 0 and -1 or 1
                line_vals['amount_cur'] = amt * sign
            vals["lines"].append(("create", line_vals))
        new_id = self.create(vals, context={"journal_id": obj.journal_id.id})
        self.post([new_id])
        new_obj = self.browse(new_id)
        return {
            "next": {
                "name": "journal_entry",
                "mode": "form",
                "active_id": new_id,
            },
            "flash":
            "Journal entry %s reversed to %s" % (obj.number, new_obj.number),
            "reverse_move_id":
            new_id,
        }