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, }
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, }
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
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
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
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
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
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, }