class InlineHelp(Model): _name = "inline.help" _string = "Help Item" _fields = { "action": fields.Char("Action Name", required=True, search=True), "title": fields.Char("Help Title", required=True, search=True), "content": fields.Text("Help Content", required=True, search=True), "hide": fields.Boolean("Hide"), "create_date": fields.DateTime("Date Created"), "modif_date": fields.DateTime("Date Modified"), } _order = "title" _defaults = { "create_date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "modif_date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), } def create(self, vals, **kw): res = super().create(vals, **kw) static.clear_translations() # XXX: rename this return res def write(self, ids, vals, **kw): super().write(ids, vals, **kw) static.clear_translations() # XXX: rename this def delete(self, ids, **kw): super().delete(ids, **kw) static.clear_translations() # XXX: rename this
class PayPeriodLine(Model): _name = "hr.pay.period.line" _string = "Pay Period Line" _fields = { 'period_id': fields.Many2One("hr.pay.period", "Pay Period", required=True), 'time_start': fields.DateTime("Time Start", required=True), 'time_stop': fields.DateTime("Time Start", required=True), }
class TaskLine(Model): _name = "my.task.line" _fields = { 'task_id': fields.Many2One("my.task", "Task", required=True, on_delete="cascade"), "description": fields.Text("Description"), "time_start": fields.DateTime("Start"), "time_end": fields.DateTime("End"), }
class WSEvent(Model): _name = "ws.event" _log_access = False _fields = { "listener_id": fields.Many2One("ws.listener", "Listener", on_delete="cascade"), "name": fields.Char("Name", required=True), "ctime": fields.DateTime("Create Time", required=True), } def new_event(self, event_name, user_id): print("WSEvent.new_event", event_name, user_id) t = time.strftime("%Y-%m-%d %H:%M:%S") db = get_connection() if user_id: res = db.query("SELECT id FROM ws_listener WHERE user_id=%s", user_id) else: res = db.query("SELECT id FROM ws_listener") for r in res: db.execute( "INSERT INTO ws_event (listener_id,name,ctime) VALUES (%s,%s,%s)", r.id, event_name, t)
class WSListener(Model): _name = "ws.listener" _log_access = False _fields = { "user_id": fields.Many2One("base.user", "User"), "last_check_time": fields.DateTime("Last Check Time", required=True), }
class PayPeriod(Model): _name = "hr.pay.period" _string = "Pay Period" _fields = { "name": fields.Char("Name", required=True, search=True), "year": fields.DateTime("Year"), 'lines': fields.One2Many("hr.pay.period.line", "period_id", "Lines"), }
class Operation(Model): _name = "production.operation" _string = "Production Operation" _fields = { "order_id": fields.Many2One("production.order", "Production Order", required=True, on_delete="cascade"), "workcenter_id": fields.Many2One("workcenter", "Workcenter", required=True), "employee_id": fields.Many2One("hr.employee", "Employee"), "planned_duration": fields.Decimal("Planned Duration (Minutes)"), "time_start": fields.DateTime("Start Time"), "time_stop": fields.DateTime("Stop Time"), "actual_duration": fields.Decimal("Actual Duration (Minutes)", function="get_actual_duration"), "notes": fields.Text("Notes"), } def get_actual_duration(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.time_start and obj.time_stop: t0 = datetime.strptime(obj.time_start, "%Y-%m-%d %H:%M:%S") t1 = datetime.strptime(obj.time_stop, "%Y-%m-%d %H:%M:%S") vals[obj.id] = math.ceil((t1 - t0).total_seconds() / 60.0) else: vals[obj.id] = None return vals
class EmailEvent(Model): _name = "email.event" _string = "Email Event" _fields = { "email_id": fields.Many2One("email.message", "Email", required=True, on_delete="cascade"), "type": fields.Selection([["accepted", "Accepted"], ["rejected", "Rejected"], ["delivered", "Delivered"], ["failed", "Failed"], ["opened", "Opened"], ["clicked", "Clicked"], ["unsubscribed", "Unsubscribed"], ["complained", "Complained"], ["stored", "Stored"]], "Event Type", required=True), "date": fields.DateTime("Date", required=True), "ip_addr": fields.Char("IP Address"), "location": fields.Char("Location"), "user_agent": fields.Char("User Agent", size=1024), "url": fields.Char("URL", size=1024), "details": fields.Text("Details"), } _order = "date" _defaults = { "date": time.strftime("%Y-%m-%d %H:%M:%S"), }
class Attach(Model): _name = "attach" _string = "Attachment" _order = "date desc" _fields = { "date": fields.DateTime("Date", required=True, search=True), "user_id": fields.Many2One("base.user", "User", search=True), "file": fields.File("File", required=True), "related_id": fields.Reference([["document", "Document"]], "Related To"), "description": fields.Text("Description", search=True), "comments": fields.One2Many("message", "related_id", "Comments"), } _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "user_id": lambda self, context: int(context.get("user_id")), }
class EcomWishList(Model): _name = "ecom.wishlist" _string = "Product Wishlist" _fields = { "name": fields.Char("Name"), "date": fields.DateTime("Date"), "product_id": fields.Many2One("product", "Product", required=True, search=True), "contact_id": fields.Many2One("contact", "Contact", required=True, search=True), } _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), } _order = "date desc"
class Feedback(Model): _name = "feedback" _fields = { "date": fields.DateTime("Date", required=True), "from_id": fields.Many2One("base.user", "From User", required=True), "feeling": fields.Selection( [["good", "Good"], ["indifferent", "Indifferent"], ["bad", "Bad"]], "Feeling"), "message": fields.Text("Message", required=True), } _order = "date" _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "from_id": lambda self, ctx: ctx.get("user_id"), }
class EcomProductReview(Model): _name = "ecom.product.review" _string = "Product Review" _fields = { "name": fields.Char("Name", required=True, search=True), "title": fields.Char("Title", required=True, search=True), "review": fields.Text("Review", required=True), "rating": fields.Selection([("0", "Not Rate"), ("1", "Bad"), ("2", "Poor"), ("3", "Regular"), ("4", "Good"), ("5", "Gorgeous")], "Rating"), "date": fields.DateTime("Date"), "state": fields.Selection([("draft", "Draft"), ("approved", "Approved"), ("discarded", "Discarded")], "State"), "product_id": fields.Many2One("product", "Product", required=True, search=True), } _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "state": "draft", "rating": "0" } _order = "date desc" def create(self, vals, **kw): id = super().create(vals, **kw) for obj in self.browse([id]): obj.trigger("created") def approve_review(self, ids, context={}): self.write(ids, {"state": "approved"}) def reset_draft(self, ids, context={}): self.write(ids, {"state": "draft"}) def discard_review(self, ids, context={}): self.write(ids, {"state": "discarded"})
class FieldCache(Model): _name = "field.cache" _string = "Field Cache" _fields = { "model": fields.Char("Model", index=True), "field": fields.Char("Field", index=True), "record_id": fields.Integer("Record ID", index=True), "value": fields.Text("Value"), "ctime": fields.DateTime("Time Created"), } _indexes = [ ("model", "field", "record_id"), ] def get_value(self, model, field, ids, min_ctime=None): db = get_connection() q = "SELECT record_id,value,ctime FROM field_cache WHERE model=%s AND field=%s AND record_id IN %s" args = [model, field, tuple(ids)] if min_ctime: q += " AND ctime>=%s" args.append(min_ctime) res = db.query(q, *args) vals = {} for r in res: vals[r.record_id] = utils.json_loads(r.value) return vals def set_value(self, model, field, record_id, value): ctime = time.strftime("%Y-%m-%d %H:%M:%S") db = get_connection() db.execute( "DELETE FROM field_cache WHERE model=%s AND field=%s AND record_id=%s", model, field, record_id) db.execute( "INSERT INTO field_cache (model,field,record_id,value,ctime) VALUES (%s,%s,%s,%s,%s)", model, field, record_id, utils.json_dumps(value), ctime) def clear_cache(self, model): db = get_connection() db.execute("DELETE FROM field_cache WHERE model=%s", model)
class Log(Model): _name = "log" _string = "Log Entry" _fields = { "date": fields.DateTime("Date", required=True, search=True), "user_id": fields.Many2One("base.user", "User", search=True), "ip_addr": fields.Char("IP Address", search=True), "country_id": fields.Many2One("country", "Country", readonly=True), "message": fields.Text("Message", required=True, search=True), "details": fields.Text("Details", search=True), "comments": fields.One2Many("message", "related_id", "Comments"), "related_id": fields.Reference([], "Related To"), } _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), } _order = "id desc" def log(self, msg, details=None, ip_addr=None, related_id=None): uid = get_active_user() if not ip_addr: ip_addr = get_ip_addr() try: country_code = get_ip_country(ip_addr) res = get_model("country").search([["code", "=", country_code]]) country_id = res[0] except Exception as e: #print("Failed to get IP country: %s"%e) country_id = None vals = { "user_id": uid, "ip_addr": ip_addr, "message": msg, "details": details, "country_id": country_id, "related_id": related_id, } set_active_user(1) self.create(vals) set_active_user(uid)
class Attendance(Model): _name = "hr.attendance" _string = "Attendance Event" _audit_log = True _fields = { "employee_id": fields.Many2One("hr.employee", "Employee", required=True, search=True), "time": fields.DateTime("Time", required=True, search=True), "action": fields.Selection([["sign_in", "Sign In"], ["sign_out", "Sign Out"]], "Action", required=True, search=True), "comments": fields.One2Many("message", "related_id", "Comments"), } _order = "time desc" _defaults = { "time": lambda *a: time.strftime("%Y-%m-%d %H:%M:S"), "user_id": lambda *a: access.get_active_user(), }
class Activity(Model): _name = "activity" _string = "Activity" _name_field = "subject" _fields = { "type": fields.Selection([["task", "Task"], ["event", "Event"], ["meeting", "Meeting"], ["call", "Call"]], "Activity Type", required=True, search=True), "user_id": fields.Many2One("base.user", "Assigned To", search=True, required=True), "subject": fields.Char("Subject", required=True, size=128, search=True), "date": fields.Date("Date", search=True), "due_date": fields.Date("Due Date"), "description": fields.Text("Description"), "body": fields.Text("Body"), "state": fields.Selection( [["new", "Not Started"], ["in_progress", "In Progress"], ["done", "Completed"], ["waiting", "Waiting on someone else"], ["deferred", "Deferred"]], "Status", required=True), "priority": fields.Selection( [["high", "High"], ["normal", "Normal"], ["low", "Low"]], "Priority"), "phone": fields.Char("Phone"), "email": fields.Char("Email"), "event_start": fields.DateTime("Start"), "event_end": fields.DateTime("End"), "location": fields.Char("Location"), "email_uid": fields.Char("Email UID"), "email_account_id": fields.Many2One("email.account", "Email Account"), "work_times": fields.One2Many("work.time", "activity_id", "Work Time"), "related_id": fields.Reference( [["contact", "Contact"], ["sale.opportunity", "Opportunity"], ["sale.quot", "Quotation"], ["sale.order", "Sales Order"], ["job", "Service Order"], ["issue", "Issue"]], "Related To"), "name_id": fields.Reference([["contact", "Contact"], ["sale.lead", "Lead"]], "Name"), "comments": fields.One2Many("message", "related_id", "Comments"), "overdue": fields.Boolean("Overdue", function="get_overdue", function_search="search_overdue"), } def _get_name_id(self, context={}): defaults = context.get("defaults") if not defaults: return related = defaults.get("related_id") if not related: return model, model_id = related.split(",") model_id = int(model_id) if model == "sale.quot": obj = get_model("sale.quot").browse(model_id) return "contact,%s" % obj.contact_id.id _defaults = { "state": "new", "date": lambda *a: time.strftime("%Y-%m-%d"), "type": lambda self, ctx: ctx.get("activ_type") or "task", "name_id": _get_name_id, } _order = "due_date desc,date desc,id desc" # XXX def view_activity(self, ids, context={}): obj = self.browse(ids[0]) return { "next": { "name": "activ", "mode": "form", "active_id": obj.id, } } def send_email(self, ids, context={}): obj = self.browse(ids)[0] from_addr = obj.user_id.email to_addr = obj.name_id.email if not to_addr: raise Exception("Email not found") res = get_model("email.account").search([["type", "=", "smtp"]]) if not res: raise Exception("Email account not found") acc_id = res[0] acc = get_model("email.account").browse(acc_id) server = smtplib.SMTP(acc.host, acc.port) if acc.user: server.login(acc.user, acc.password) msg = "From: " + from_addr + "\r\n" msg += "To: " + to_addr + "\r\n" msg += "Subject: " + obj.subject + "\r\n\r\n" msg += obj.body server.sendmail(from_addr, [to_addr], msg) obj.write({"state": "done"}) server.quit() # XXX: move this def fetch_email(self, context={}): print("fetch_email") acc_ids = get_model("email.account").search([["type", "=", "pop3"]]) for acc in get_model("email.account").browse(acc_ids): print("connecting %s %s %s" % (acc.host, acc.port, acc.user)) if acc.security == "ssl": serv = poplib.POP3_SSL(acc.host, acc.port) else: serv = poplib.POP3(acc.host, acc.port) serv.user(acc.user) if acc.password: serv.pass_(acc.password) try: resp, msg_list, size = serv.uidl() print("%d messages" % len(msg_list)) for msg_info in msg_list: msg_no, msg_uid = msg_info.decode().split() print("msg_no", msg_no) print("msg_uid", msg_uid) try: res = self.search_read( [["email_account_id", "=", acc.id], ["email_uid", "=", msg_uid]]) if res: print("skipping %s" % msg_uid) serv.dele(msg_no) continue print("reading %s" % msg_uid) resp, lines, size = serv.retr(msg_no) msg = email.message_from_bytes(b"\n".join(lines)) def dec_header(data): dh = decode_header(data or "") s = "" for data, charset in dh: if isinstance(data, str): s += data else: s += data.decode( conv_charset(charset) or "utf-8") return s def dec_date(data): res = parsedate(data or "") if not res: return "" return time.strftime("%Y-%m-%d %H:%M:%S", res) def get_body(m): if m.get_filename(): return "[Attachment: %s (%s)]\n" % (dec_header( m.get_filename()), m.get_content_type()) else: if not m.is_multipart(): charset = conv_charset( m.get_content_charset()) return m.get_payload(decode=True).decode( charset or "utf-8", errors="replace") else: data = m.get_payload() found = False res = [] for m in data: fn = m.get_filename() if not fn: if found: continue found = True res.append(get_body(m)) return "\n".join(res) email_vals = { "account_id": acc.id, "msg_uid": msg_uid, "date": dec_date(msg["Date"]), "from_addr": parseaddr(msg["From"])[1][:64], "to_addr": parseaddr(msg["To"])[1][:64], "subject": dec_header(msg["Subject"])[:128], "body": get_body(msg), } get_model("activity").import_email(email_vals) except Exception as e: print("WARNING: failed to import email %s", msg_uid) finally: serv.dele(msg_no) finally: print("quitting") db = database.get_connection() db.commit() serv.quit() # XXX: move this def import_email(self, email): print("import_email") print("from=%s to=%s subject=%s" % (email["from_addr"], email["to_addr"], email["subject"])) from_user_id = None from_contact_id = None from_lead_id = None to_user_id = None to_contact_id = None to_lead_id = None user_id = None contact_id = None lead_id = None contact_id = None opport_id = None quot_id = None sale_id = None name_id = None related_id = None res = get_model("base.user").search( [["email", "=ilike", email["from_addr"]]]) if res: from_user_id = res[0] res = get_model("contact").search( [["email", "=ilike", email["from_addr"]]]) if res: from_contact_id = res[0] else: res = get_model("sale.lead").search( [["email", "=ilike", email["from_addr"]]]) if res: from_lead_id = res[0] if not from_user_id and not from_contact_id and not from_lead_id: print(" => skipping (from)") return res = get_model("base.user").search( [["email", "=ilike", email["to_addr"]]]) if res: to_user_id = res[0] res = get_model("contact").search( [["email", "=ilike", email["to_addr"]]]) if res: to_contact_id = res[0] else: res = get_model("sale.lead").search( [["email", "=ilike", email["to_addr"]]]) if res: to_lead_id = res[0] if (from_contact_id or from_lead_id) and to_user_id: if from_contact_id: contact_id = from_contact_id if from_lead_id: lead_id = from_lead_id user_id = to_user_id elif from_user_id and (to_contact_id or to_lead_id): user_id = from_user_id if to_contact_id: contact_id = to_contact_id if to_lead_id: lead_id = to_lead_id else: print(" => skipping (from/to)") return if contact_id: contact = get_model("contact").browse(contact_id) if contact.contact_id: contact_id = contact.contact_id.id if contact_id and email["subject"]: subj = email["subject"].lower() opp_ids = get_model("sale.opportunity").search( [["contact_id", "=", contact_id], ["state", "=", "open"]]) for opp in get_model("sale.opportunity").browse(opp_ids): if subj.find(opp.name.lower()) != -1: opport_id = opp.id break quot_ids = get_model("sale.quot").search( [["contact_id", "=", contact_id], ["state", "=", "approved"]]) for quot in get_model("sale.quot").browse(quot_ids): if subj.find(quot.number.lower()) != -1: quot_id = quot.id break sale_ids = get_model("sale.order").search( [["contact_id", "=", contact_id], ["state", "=", "confirmed"]]) for sale in get_model("sale.order").browse(sale_ids): if subj.find(sale.number.lower()) != -1: sale_id = sale.id break if contact_id: name_id = "contact,%d" % contact_id elif lead_id: name_id = "sale.lead,%d" % lead_id if opport_id: related_id = "sale.opportunity,%d" % opport_id elif quot_id: related_id = "sale.quot,%d" % quot_id elif sale_id: related_id = "sale.order,%d" % sale_id elif contact_id: related_id = "contact,%d" % contact_id vals = { "type": "email", "subject": email["subject"], "state": "done", "user_id": user_id, "related_id": related_id, "name_id": name_id, "email_uid": email["msg_uid"], "email_account_id": email["account_id"], "body": email["body"], "date": email["date"], } act_id = get_model("activity").create(vals) print(" => new activity %d" % act_id) return act_id def get_overdue(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.due_date: vals[obj.id] = obj.due_date < time.strftime( "%Y-%m-%d") and obj.state != "done" else: vals[obj.id] = False return vals def search_overdue(self, clause, context={}): return [["due_date", "<", time.strftime("%Y-%m-%d")], ["state", "!=", "done"]] def check_days_before_overdue(self, ids, days=None, days_from=None, days_to=None, context={}): print("Activity.check_days_before_overdue", ids, days, days_from, days_to) cond = [["state", "!=", "done"]] if days != None: d = (datetime.date.today() + datetime.timedelta(days=days)).strftime("%Y-%m-%d") cond.append(["due_date", "=", d]) if days_from != None: d = (datetime.date.today() + datetime.timedelta(days=days_from)).strftime("%Y-%m-%d") print("XXXXXXXXXXXXXxx d", d) cond.append(["due_date", "<=", d]) if days_to != None: d = (datetime.date.today() + datetime.timedelta(days=days_to)).strftime("%Y-%m-%d") cond.append(["due_date", ">=", d]) if ids: cond.append(["ids", "in", ids]) ids = self.search(cond) return ids
class StockBalance(Model): _name = "stock.balance" _string = "Stock Balance" _fields = { "product_id": fields.Many2One("product", "Product", required=True, search=True), "lot_id": fields.Many2One("stock.lot", "Lot / Serial Number", search=True), "location_id": fields.Many2One("stock.location", "Location", required=True, search=True), "container_id": fields.Many2One("stock.container", "Container", search=True), "qty_phys": fields.Decimal("Physical Qty", required=True), "qty_virt": fields.Decimal("Virtual Qty", required=True), "uom_id": fields.Many2One("uom", "UoM", required=True), "min_qty": fields.Decimal("Min Qty"), "below_min": fields.Boolean("Below Min", search=True), "amount": fields.Decimal("Amount"), "last_change": fields.DateTime("Last Change"), "supplier_id": fields.Many2One("contact", "Supplier", store=False, search=True, function_search="search_supplier"), "qty2": fields.Decimal("Secondary Qty"), } _order = "product_id.code,location_id" _sql_constraints = [ ("prod_loc_uniq", "unique (product_id, location_id, lot_id, container_id)", "Stock balances must have unique products and locations!"), ] def read(self, *a, **kw): self.update_balances() res = super().read(*a, **kw) return res def search(self, *a, **kw): self.update_balances() res = super().search(*a, **kw) return res def update_balances(self, context={}): #print("UPDATE_BALANCES") user_id=access.get_active_user() access.set_active_user(1) try: db = database.get_connection() prod_ids = get_model("product").search( [["update_balance", "=", True], ["type", "=", "stock"]], context={"active_test": False}) # XXX if not prod_ids: return print("prod_ids", prod_ids) db.execute("LOCK stock_balance IN EXCLUSIVE MODE") db.execute("DELETE FROM stock_balance WHERE product_id IN %s", tuple(prod_ids)) loc_ids = get_model("stock.location").search([["type", "=", "internal"]]) if not loc_ids: return prod_uoms = {} res = db.query("SELECT id,uom_id FROM product WHERE id IN %s", tuple(prod_ids)) for r in res: prod_uoms[r.id] = r.uom_id min_qtys = {} res = db.query( "SELECT location_id,product_id,min_qty,uom_id FROM stock_orderpoint WHERE product_id IN %s", tuple(prod_ids)) for r in res: min_qtys[(r.location_id, r.product_id)] = (r.min_qty, r.uom_id) qtys = {} res = db.query( "SELECT location_to_id,container_to_id,product_id,lot_id,uom_id,state,sum(qty) AS total_qty,sum(cost_amount) AS total_amt,max(date) AS max_date,SUM(qty2) AS total_qty2 FROM stock_move WHERE product_id IN %s AND location_to_id IN %s AND state IN ('pending','approved','done') GROUP BY location_to_id,container_to_id,product_id,lot_id,uom_id,state", tuple(prod_ids), tuple(loc_ids)) for r in res: qtys.setdefault((r.location_to_id, r.container_to_id, r.product_id, r.lot_id), []).append( ("in", r.total_qty, r.total_amt or 0, r.uom_id, r.state, r.max_date, r.total_qty2 or 0)) res = db.query( "SELECT location_from_id,container_from_id,product_id,lot_id,uom_id,state,sum(qty) AS total_qty,sum(cost_amount) AS total_amt,max(date) AS max_date,SUM(qty2) AS total_qty2 FROM stock_move WHERE product_id IN %s AND location_from_id IN %s AND state IN ('pending','approved','done') GROUP BY location_from_id,container_from_id,product_id,lot_id,uom_id,state", tuple(prod_ids), tuple(loc_ids)) for r in res: qtys.setdefault((r.location_from_id, r.container_from_id, r.product_id, r.lot_id), []).append( ("out", r.total_qty, r.total_amt or 0, r.uom_id, r.state, r.max_date, r.total_qty2 or 0)) bals = {} prod_loc_qtys = {} for (loc_id, cont_id, prod_id, lot_id), totals in qtys.items(): last_change = None for type, qty, amt, uom_id, state, max_date, qty2 in totals: last_change = last_change and max(max_date, last_change) or max_date res = min_qtys.get((loc_id, prod_id)) if res: min_qty, min_uom_id = res else: min_qty = 0 min_uom_id = None bal_uom_id = prod_uoms[prod_id] state_qtys = {} state_amts = {} state_qtys2 = {} for type, qty, amt, uom_id, state, max_date, qty2 in totals: state_qtys.setdefault(state, 0) state_amts.setdefault(state, 0) state_qtys2.setdefault(state, 0) qty_conv = get_model("uom").convert(qty, uom_id, bal_uom_id) if type == "in": state_qtys[state] += qty_conv state_amts[state] += amt state_qtys2[state] += qty2 elif type == "out": state_qtys[state] -= qty_conv state_amts[state] -= amt state_qtys2[state] -= qty2 qty_virt = state_qtys.get("done", 0) + state_qtys.get("pending", 0) + state_qtys.get("approved", 0) bals[(loc_id, cont_id, prod_id, lot_id)] = { "qty_phys": state_qtys.get("done", 0), "qty_virt": qty_virt, "amt": state_amts.get("done", 0), "last_change": last_change, "uom_id": bal_uom_id, "min_qty": min_qty and get_model("uom").convert(min_qty, min_uom_id, bal_uom_id) or 0, "qty2": state_qtys2.get("done", 0), } prod_loc_qtys.setdefault((loc_id, prod_id), 0) prod_loc_qtys[(loc_id, prod_id)] += qty_virt for (loc_id, prod_id), (min_qty, uom_id) in min_qtys.items(): if (loc_id, prod_id) not in prod_loc_qtys: bals[(loc_id, None, prod_id, None)] = { "qty_phys": 0, "qty_virt": 0, "amt": 0, "min_qty": min_qty, "uom_id": uom_id, "last_change": None, "qty2": 0, } parent_locs = {} for loc in get_model("stock.location").search_browse([["parent_id", "!=", None]]): parent_locs[loc.id] = loc.parent_id.id for (loc_id, cont_id, prod_id, lot_id), bal_vals in list(bals.items()): child_id = loc_id while True: parent_id = parent_locs.get(child_id) if not parent_id: break k = (parent_id, cont_id, prod_id, lot_id) if k not in bals: bals[k] = { "qty_phys": 0, "qty_virt": 0, "amt": 0, "min_qty": 0, "uom_id": prod_uoms[prod_id], "last_change": None, "qty2": 0, } parent_vals = bals[k] parent_vals["qty_phys"] += bal_vals["qty_phys"] parent_vals["qty_virt"] += bal_vals["qty_virt"] parent_vals["amt"] += bal_vals["amt"] parent_vals["min_qty"] += bal_vals["min_qty"] if bal_vals["last_change"] and (not parent_vals["last_change"] or parent_vals["last_change"] < bal_vals["last_change"]): parent_vals["last_change"] = bal_vals["last_change"] parent_vals["qty2"] += bal_vals["qty2"] child_id = parent_id total_virt_qtys={} for (loc_id, cont_id, prod_id, lot_id), bal_vals in bals.items(): total_virt_qtys.setdefault(prod_id,0) total_virt_qtys[prod_id]+=bal_vals["qty_virt"] below_prods=set() for prod_id,qty_virt in total_virt_qtys.items(): if qty_virt<0: # XXX: take into account min stock rules below_prods.add(prod_id) for (loc_id, cont_id, prod_id, lot_id), bal_vals in bals.items(): bal_vals["below_min"]=prod_id in below_prods for (loc_id, cont_id, prod_id, lot_id), bal_vals in bals.items(): qty_phys = bal_vals["qty_phys"] qty_virt = bal_vals["qty_virt"] qty2 = bal_vals["qty2"] amt = bal_vals["amt"] min_qty = bal_vals["min_qty"] if qty_phys == 0 and qty_virt == 0 and min_qty == 0 and amt == 0: continue prod_loc_qty = prod_loc_qtys.get((loc_id, prod_id), 0) below_min = bal_vals["below_min"] uom_id = bal_vals["uom_id"] last_change = bal_vals["last_change"] db.execute("INSERT INTO stock_balance (location_id,container_id,product_id,lot_id,qty_phys,qty_virt,amount,min_qty,uom_id,below_min,last_change,qty2) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)", loc_id, cont_id, prod_id, lot_id, qty_phys, qty_virt, amt, min_qty, uom_id, below_min, last_change, qty2) get_model("product").write(prod_ids, {"update_balance": False}) finally: access.set_active_user(user_id) def get_qty_phys(self, location_id, product_id, lot_id): cond=[["location_id", "=", location_id], ["product_id", "=", product_id]] if lot_id: cond.append(["lot_id","=",lot_id]) qty=0 for bal in self.search_browse(cond): qty+=bal.qty_phys return qty def get_qty_virt(self, location_id, product_id, lot_id): cond=[["location_id", "=", location_id], ["product_id", "=", product_id]] if lot_id: cond.append(["lot_id","=",lot_id]) qty=0 for bal in self.search_browse(cond): qty+=bal.qty_virt return qty def get_unit_price(self, location_id, product_id): res = self.search([["location_id", "=", location_id], ["product_id", "=", product_id]]) if not res: return 0 obj = self.browse(res)[0] if obj.qty_phys: unit_price = obj.amount / obj.qty_phys else: unit_price = 0 return unit_price def get_prod_qty(self, product_id, loc_type="internal"): ids = self.search([["location_id.type", "=", loc_type], ["product_id", "=", product_id]]) qty = 0 for obj in self.browse(ids): qty += obj.qty_phys return qty def make_po(self, ids, context={}): suppliers = {} for obj in self.browse(ids): if obj.qty_virt >= obj.min_qty: continue prod = obj.product_id if prod.supply_method!="purchase": raise Exception("Supply method for product %s is not set to 'Purchase'"%prod.code) res = get_model("stock.orderpoint").search([["product_id", "=", prod.id]]) if res: op = get_model("stock.orderpoint").browse(res)[0] max_qty = op.max_qty else: max_qty = 0 diff_qty = max_qty - obj.qty_virt if prod.purchase_uom_id: purch_uom=prod.purchase_uom_id if not prod.purchase_to_stock_uom_factor: raise Exception("Missing purchase order -> stock uom factor for product %s"%prod.code) purch_qty=diff_qty/prod.purchase_to_stock_uom_factor else: purch_uom=prod.uom_id purch_qty=diff_qty if prod.purchase_qty_multiple: n=math.ceil(purch_qty/prod.purchase_qty_multiple) purch_qty=n*prod.purchase_qty_multiple if prod.purchase_uom_id: qty_stock=purch_qty*prod.purchase_to_stock_uom_factor else: qty_stock=None line_vals = { "product_id": prod.id, "description": prod.name_get()[0][1], "qty": purch_qty, "uom_id": purch_uom.id, "unit_price": prod.purchase_price or 0, "tax_id": prod.purchase_tax_id.id, "qty_stock": qty_stock, } if not prod.suppliers: raise Exception("Missing default supplier for product %s" % prod.name) contact_id = prod.suppliers[0].supplier_id.id suppliers.setdefault(contact_id, []).append(line_vals) if not suppliers: raise Exception("Nothing to order") count = 0 for contact_id, lines in suppliers.items(): vals = { "contact_id": contact_id, "lines": [("create", x) for x in lines], } purch_id = get_model("purchase.order").create(vals) count += 1 return { "next": { "name": "purchase", "tab": "Draft", }, "flash": "%d purchase orders created" % count, } def make_transfer(self, ids, context={}): if not ids: return first = self.browse(ids)[0] vals = { "location_from_id": first.location_id.id, } lines = [] for obj in self.browse(ids): lines.append({ "product_id": obj.product_id.id, "lot_id": obj.lot_id.id, "qty": obj.qty_phys, "uom_id": obj.uom_id.id, "container_from_id": obj.container_id.id, "container_to_id": obj.container_id.id, }) vals["lines"] = [("create", v) for v in lines] new_id = get_model("barcode.transfer").create(vals) return { "next": { "name": "barcode_transfer", "active_id": new_id, }, } def make_issue(self, ids, context={}): if not ids: return first = self.browse(ids)[0] vals = { "location_from_id": first.location_id.id, } lines = [] for obj in self.browse(ids): lines.append({ "product_id": obj.product_id.id, "lot_id": obj.lot_id.id, "qty": obj.qty_phys, "uom_id": obj.uom_id.id, "container_from_id": obj.container_id.id, }) vals["lines"] = [("create", v) for v in lines] new_id = get_model("barcode.issue").create(vals) return { "next": { "name": "barcode_issue", "active_id": new_id, }, } def search_supplier(self, clause, context={}): supplier_id = clause[2] contact = get_model("contact").browse(supplier_id) prod_ids = [p.product_id.id for p in contact.supplied_products] return [["product_id", "in", prod_ids]] def get_totals(self, product_ids=None, location_ids=None, lot_ids=None, container_ids=None, date_from=None, date_to=None, virt_stock=False): print("stock_balance.get_totals product_ids=%s location_ids=%s lot_ids=%s container_ids=%s date_from=%s date_to=%s" % ( product_ids, location_ids, lot_ids, container_ids, date_from, date_to)) t0 = time.time() q = "SELECT product_id,lot_id,location_from_id,container_from_id,location_to_id,container_to_id,uom_id,SUM(qty) AS total_qty,SUM(cost_amount) AS total_amt,SUM(qty2) AS total_qty2 FROM stock_move WHERE" if virt_stock: q+=" state in ('pending','approved','done')" else: q+=" state='done'" q_args = [] if product_ids is not None: if product_ids: q += " AND product_id IN %s" q_args.append(tuple(product_ids)) else: q += " AND false" if location_ids is not None: if location_ids: q += " AND (location_from_id IN %s OR location_to_id IN %s)" q_args += [tuple(location_ids), tuple(location_ids)] else: q += " AND false" if lot_ids is not None: if lot_ids: q += " AND lot_id IN %s" q_args.append(tuple(lot_ids)) else: q += " AND false" if container_ids is not None: if container_ids: q += " AND (container_from_id IN %s OR container_to_id IN %s)" q_args += [tuple(container_ids), tuple(container_ids)] else: q += " AND false" if date_from: q += " AND date>=%s" q_args.append(date_from) if date_to: q += " AND date<=%s" q_args.append(date_to) q += " GROUP BY product_id,lot_id,location_from_id,container_from_id,location_to_id,container_to_id,uom_id" db = database.get_connection() res = db.query(q, *q_args) totals = {} prod_ids = set() for r in res: prod_ids.add(r.product_id) prod_ids = list(prod_ids) prod_uoms = {} for prod in get_model("product").browse(prod_ids): prod_uoms[prod.id] = prod.uom_id.id for r in res: qty = get_model("uom").convert(r.total_qty, r.uom_id, prod_uoms[r.product_id]) amt = r.total_amt or 0 qty2 = r.total_qty2 or 0 k = (r.product_id, r.lot_id, r.location_from_id, r.container_from_id, r.location_to_id, r.container_to_id) tot = totals.setdefault(k, [0, 0, 0]) tot[0] += qty tot[1] += amt tot[2] += qty2 t1 = time.time() print("totals size: %d" % len(totals)) print("get_totals finished in %s ms" % ((t1 - t0) * 1000)) return totals def compute_key_balances(self, keys, context={}): print("stock_balance.compute_key_balances", keys) t0 = time.time() all_prod_ids = set() all_loc_ids = set() for prod_id, lot_id, loc_id, cont_id in keys: all_prod_ids.add(prod_id) all_loc_ids.add(loc_id) all_prod_ids = list(all_prod_ids) all_loc_ids = list(all_loc_ids) date_to=context.get("date_to") virt_stock=context.get("virt_stock") tots = self.get_totals(product_ids=all_prod_ids, location_ids=all_loc_ids, date_to=date_to, virt_stock=virt_stock) prod_tots = {} def get_sum(prod, lot=None, loc_from=None, loc_to=None, cont_from=None, cont_to=None): tot_qty = 0 tot_amt = 0 tot_qty2 = 0 if prod in prod_tots: # XXX: can still improve speed for (lot_id, loc_from_id, cont_from_id, loc_to_id, cont_to_id), (qty, amt, qty2) in prod_tots[prod].items(): if lot and lot_id != lot: continue if loc_from and loc_from_id != loc_from: continue if loc_to and loc_to_id != loc_to: continue if cont_from and cont_from_id != cont_from: continue if cont_to and cont_to_id != cont_to: continue tot_qty += qty tot_amt += amt tot_qty2 += qty2 return tot_qty, tot_amt, tot_qty2 for (prod_id, lot_id, loc_from_id, cont_from_id, loc_to_id, cont_to_id), (qty, amt, qty2) in tots.items(): prod_tots.setdefault( prod_id, {})[(lot_id, loc_from_id, cont_from_id, loc_to_id, cont_to_id)] = (qty, amt, qty2) bals = {} for key in keys: prod_id, lot_id, loc_id, cont_id = key qty_in, amt_in, qty2_in = get_sum(prod=prod_id, lot=lot_id, loc_to=loc_id, cont_to=cont_id) qty_out, amt_out, qty2_out = get_sum(prod=prod_id, lot=lot_id, loc_from=loc_id, cont_from=cont_id) qty = qty_in - qty_out amt = amt_in - amt_out qty2 = qty2_in - qty2_out bals[key] = [qty, amt, qty2] t1 = time.time() print("compute_key_balances finished in %d ms" % ((t1 - t0) * 1000)) return bals
class CronJob(Model): _name = "cron.job" _string = "Job" _fields = { "name": fields.Char("Description", required=True), "date": fields.DateTime("Scheduled Date"), "model": fields.Char("Model"), "method": fields.Char("Method"), "args": fields.Text("Arguments"), "state": fields.Selection([["waiting", "Waiting"], ["running", "Running"], ["done", "Done"], ["canceled", "Canceled"]], "Status", required=True), "interval_num": fields.Integer("Interval Number"), "interval_type": fields.Selection([["second", "Second"], ["minute", "Minute"], ["hour", "Hour"], ["day", "Day"]], "Interval Unit"), "call_num": fields.Integer("Number Of Calls"), "date_start": fields.DateTime("Start Date"), "date_stop": fields.DateTime("Stop Date"), "comments": fields.One2Many("message", "related_id", "Comments"), "last_error_time": fields.DateTime("Last Error Time"), "error_message": fields.Text("Error Message"), "timeout": fields.Integer("Timeout (s)"), } _order = "date desc" _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "state": "waiting", } def create(self, vals, **kw): new_id = super().create(vals, **kw) netforce.job.force_check_jobs() return new_id def write(self, ids, vals, **kw): super().write(ids, vals, **kw) netforce.job.force_check_jobs() def schedule_job(self, model, method, *args): vals = { "name": "/", "model": model, "method": method, "args": json.dumps(args), } return self.create(vals) def trigger_event(self, event, context={}): print("CronJob.trigger_event", event) db = get_connection() res = db.query( "SELECT DISTINCT(tm.name) AS trigger_model FROM wkf_rule r,model tm WHERE tm.id=r.trigger_model_id AND r.trigger_event=%s AND r.state='active'", event) models = sorted([r.trigger_model for r in res]) for model in models: m = get_model(model) m.trigger(None, event) def get_alert(self, context={}): res = self.search([["state", "=", "error"]]) if not res: return None return { "type": "error", "title": "WARNING", "text": "There are scheduled jobs with 'Error' status", }
class Task(Model): _name = "task" _string = "Task" _name_field = "title" _fields = { "number": fields.Char("Number", required=True, search=True), "sequence": fields.Integer("Sequence"), "date_created": fields.DateTime("Date Created", required=True, search=True), "project_id": fields.Many2One("project", "Project", required=True, search=True), "milestone_id": fields.Many2One("project.milestone", "Milestone", search=True), "task_list_id": fields.Many2One("task.list", "Task List", search=True), "job_id": fields.Many2One("job", "Service Order", search=True), "contact_id": fields.Many2One("contact", "Customer", function="_get_related", function_context={"path": "project_id.contact_id"}), "title": fields.Char("Title", required=True, search=True), "description": fields.Text("Description", search=True), "progress": fields.Integer("Progress (%)"), "date_start": fields.Date("Start Date", required=True), "date_end": fields.Date("End Date", function="get_end_date", store=True), "duration": fields.Integer("Duration (Days)", required=True), "due_date": fields.Date("Due Date"), "done_date": fields.Date("Completion Date"), "resource_id": fields.Many2One("service.resource", "Assigned To"), # XXX: deprecated "documents": fields.One2Many("document", "related_id", "Documents"), "emails": fields.One2Many("email.message", "related_id", "Emails"), "comments": fields.Text("Comments"), "messages": fields.One2Many("message", "related_id", "Messages"), "emails": fields.One2Many("email.message", "related_id", "Emails"), "state": fields.Selection([["open", "Open"], ["closed", "Closed"]], "Status", required=True, search=True), "depends": fields.One2Many("task.depend", "task_id", "Task Dependencies"), "related_id": fields.Reference([["job", "Job"]], "Related To"), "depends_json": fields.Text("Task Dependencies (String)", function="get_depends_json"), "assignments": fields.One2Many("task.assign", "task_id", "Assignments"), } _order = "project_id.start_date,milestone_id.plan_date_from,task_list_id.date_created,date_start,id" # XXX def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence(type="task") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id) user_id = access.get_active_user() access.set_active_user(1) res = self.search([["number", "=", num]]) access.set_active_user(user_id) if not res: return num get_model("sequence").increment_number(seq_id) _defaults = { "date_created": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "state": "new", "number": _get_number, } def get_depends_json(self, ids, context={}): vals = {} for obj in self.browse(ids): res = [] for dep in obj.depends: res.append((dep.id, dep.prev_task_id.id, dep.delay)) vals[obj.id] = res return vals def calc_end_date(self, date_start, duration): d = datetime.strptime(date_start, "%Y-%m-%d") dur = 0 while True: if not is_holiday(d): dur += 1 if dur >= duration: break d += timedelta(days=1) return d.strftime("%Y-%m-%d") def update_end(self, context={}): data = context.get("data", {}) date_start = data["date_start"] duration = data.get("duration", 0) data["date_end"] = self.calc_end_date(date_start, duration) return data def get_end_date(self, ids, context={}): vals = {} for obj in self.browse(ids): vals[obj.id] = self.calc_end_date(obj.date_start, obj.duration) return vals def create(self, vals, *args, **kw): new_id = super().create(vals, *args, **kw) self.function_store([new_id]) return new_id def write(self, ids, *args, **kw): super().write(ids, *args, **kw) self.function_store(ids) def add_link(self, source_id, target_id, context={}): vals = { "prev_task_id": source_id, "task_id": target_id, } get_model("task.depend").create(vals) def delete_link(self, link_ids, context={}): get_model("task.depend").delete(link_ids)
class Employee(Model): _name = "hr.employee" _string = "Employee" _name_field = "first_name" # XXX _multi_company = True _key = ["code", "company_id"] _export_field = "code" _fields = { "code": fields.Char("Employee Code", search=True), "department_id": fields.Many2One("hr.department", "Department", search=True), "title": fields.Selection( [["mr", "Mr."], ["mrs", "Mrs."], ["miss", "Miss"], ["ms", "Ms."]], "Title"), "first_name": fields.Char("First Name", search=True, translate=True), "last_name": fields.Char("Last Name", required=True, search=True, translate=True), "hire_date": fields.Date("Hire Date"), "work_status": fields.Selection([["working", "Working"], ["dismissed", "Dismissed"], ["resigned", "Resigned"], ["died", "Died"]], "Work Status"), "work_type": fields.Selection( [["monthly", "Monthly"], ["daily", "Daily"], ["hourly", "Job"]], "Work Type"), "resign_date": fields.Date("Resign Date"), "position": fields.Char("Position", search=True), "birth_date": fields.Date("Birth Date"), "age": fields.Integer("Age", function="get_age"), "gender": fields.Selection([["male", "Male"], ["female", "Female"]], "Gender"), "marital_status": fields.Selection([["single", "Single"], ["married", "Married"], ["divorced", "Divorced"], ["widowed", "Widowed"]], "Marital Status"), "addresses": fields.One2Many("address", "employee_id", "Address"), "id_no": fields.Char("ID No."), "drive_license_type": fields.Selection([["car", "Car"], ['motorcycle', 'Motorcycle']], "Driving License"), "drive_license_no": fields.Char("Driving License No."), "country_id": fields.Many2One("country", "Country"), "bank_account": fields.Char("Bank Account"), "salary": fields.Decimal("Salary"), "picture": fields.File("Picture"), "tax_no": fields.Char("Taxpayer ID No."), "spouse_first_name": fields.Char("Spouse First Name"), "spouse_last_name": fields.Char("Spouse Last Name"), "spouse_title": fields.Selection([["mr", "Mr."], ["ms", "Ms."]], "Spouse Title"), "spouse_birth_date": fields.Date("Spouse Birth Date"), "spouse_tax_no": fields.Char("Spouse Tax ID No"), "spouse_status": fields.Selection( [["married", "Married existed throughout this tax year"], ["married_new", "Married during this tax year"], ["divorced", "Divorced during tax year"], ["deceased", "Deceased during tax year"]], "Spouse Status"), "spouse_filing_status": fields.Selection( [["joint", "Has income and file joint return"], ["separate", "Has income and file separate tax return"], ["no_income", "Has no income"]], "Spouse Filing Status"), "num_child1": fields.Integer("No. of Children #1 (C3)"), "num_child2": fields.Integer("No. of Children #2 (C3)"), "social_no": fields.Char("Social No."), "social_register": fields.Boolean("Register Soc. Secur."), "social_calc_method": fields.Selection( [["regular", "Regular Rate"], ["none", "Not Participate"], ["special", "Special Rate"]], "Calc. Method"), "prov_fund_no": fields.Char("Prov. Fund No."), "prov_open_date": fields.Char("Opened Prov. Fund A/C Date"), "prov_rate_employer": fields.Decimal("Employer Contribution (%)"), "prov_rate_employee": fields.Decimal("Employee Contribution (%)"), "gov_pension_fund": fields.Decimal("Gov. Pension Fund Amount (B2)"), "teacher_fund": fields.Decimal("Teacher Aid Fund Amount (B3)"), "old_disabled": fields.Decimal("Older than 65 or disabled (personal, B4)"), "old_disabled_spouse": fields.Decimal("Older than 65 or disabled (spouse, B5)"), "severance_pay": fields.Decimal("Severance Pay (B6)"), "education_donation": fields.Decimal("Education Donations (A8)"), "other_donation": fields.Decimal("Other Donations (A10)"), "house_deduct": fields.Decimal("Exemption for home buyer (A13)"), "wht_amount": fields.Decimal("Withholding Tax Amount (A15)"), "father_id_no": fields.Char("Father ID No. (C4)"), "mother_id_no": fields.Char("Mother ID No. (C4)"), "spouse_father_id_no": fields.Char("Father of spouse ID No. (C4)"), "spouse_mother_id_no": fields.Char("Mother of spouse ID No. (C4)"), "disabled_support": fields.Decimal("Disabled person support (C5)"), "parent_health_insurance": fields.Decimal("Parent Health Insurance (C6)"), "life_insurance": fields.Decimal("Life Insurance (C7)"), "retirement_mutual_fund": fields.Decimal("Retirement Mutual Fund (C9)"), "long_term_equity_fund": fields.Decimal("Long Term Equity Fund (C10)"), "interest_residence": fields.Decimal("Interest paid for residence (C11)"), "other_deduct": fields.Decimal("Other Deductions (C12)"), "comments": fields.One2Many("message", "related_id", "Comments"), "active": fields.Boolean("Active"), "time_in": fields.DateTime("Last Sign In", function="get_attend", function_multi=True), "time_out": fields.DateTime("Last Sign Out", function="get_attend", function_multi=True), "attend_state": fields.Selection([["absent", "Absent"], ["present", "Present"]], "Status", function="get_attend", function_multi=True), "user_id": fields.Many2One("base.user", "User", search=True), "payslips": fields.One2Many("hr.payslip", "employee_id", "Payslips"), "documents": fields.One2Many("document", "related_id", "Documents"), "phone": fields.Char("Phone", search=True), "approver_id": fields.Many2One("base.user", "Approver"), "company_id": fields.Many2One("company", "Company"), "leave_types": fields.Many2Many("hr.leave.type", "Leave Types"), "attendance_id": fields.Integer("Attendance ID"), "email": fields.Char("Email", search=True), 'profile_id': fields.Many2One("hr.payitem.profile", "Pay Item Profile"), 'schedule_id': fields.Many2One("hr.schedule", "Working Schedule"), 'leaves': fields.One2Many('hr.leave', 'employee_id', 'Leaves'), } def _get_code(self, context={}): while 1: code = get_model("sequence").get_number("employee") if not code: return None res = self.search([["code", "=", code]]) if not res: return code get_model("sequence").increment("employee") _defaults = { "active": True, "work_status": "working", "code": _get_code, "company_id": lambda *a: get_active_company(), } _order = "code,first_name,last_name" def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids): if obj.first_name: name = obj.first_name + " " + obj.last_name else: name = obj.last_name if obj.code: name += " [%s]" % obj.code vals.append((obj.id, name)) return vals def name_search(self, name, condition=[], limit=None, context={}): cond = [[ "or", ["first_name", "ilike", "%" + name + "%"], ["last_name", "ilike", "%" + name + "%"], ["code", "ilike", "%" + name + "%"] ], condition] ids = self.search(cond, limit=limit) return self.name_get(ids, context) def get_age(self, ids, context={}): vals = {} cr_year = int(time.strftime('%Y')) for obj in self.browse(ids): if obj.birth_date: age = cr_year - int(obj.birth_date[0:4]) else: age = 0 vals[obj.id] = age return vals def get_attend(self, ids, context={}): vals = {} for obj in self.browse(ids): # user_id=obj.user_id.id # if user_id: # db=database.get_connection() #res=db.get("SELECT MAX(time) AS time_in FROM hr_attendance WHERE user_id=%s AND action='sign_in'",user_id) # time_in=res.time_in #res=db.get("SELECT MAX(time) AS time_out FROM hr_attendance WHERE user_id=%s AND action='sign_out'",user_id) # time_out=res.time_out # else: # time_in=None # time_out=None db = database.get_connection() res = db.get( "SELECT MAX(time) AS time_in FROM hr_attendance WHERE employee_id=%s AND action='sign_in'", obj.id) time_in = res.time_in res = db.get( "SELECT MAX(time) AS time_out FROM hr_attendance WHERE employee_id=%s AND action='sign_out'", obj.id) time_out = res.time_out if time_in: if time_out and time_out > time_in: state = "absent" else: today = time.strftime("%Y-%m-%d") if time_in.startswith(today): state = "present" else: state = "absent" # should not show timeout of anotherday # if not time_out.startswith(today): # time_out=None else: state = "absent" vals[obj.id] = { "time_in": time_in, "time_out": time_out, "attend_state": state, } return vals def get_address(self, ids, context={}): obj = self.browse(ids)[0] if not obj.addresses: return "" addr = obj.addresses[0] res = addr.get_address_text() return res[addr.id] def onchange_num_child(self, context={}): data = context["data"] setting = get_model("hr.payroll.settings").browse(1) child_alw_limit = setting.child_alw_limit or 0 child_total = (data['num_child1'] or 0) + (data['num_child2'] or 0) if child_alw_limit and child_total > child_alw_limit: data['num_child1'] = 0 data['num_child2'] = 0 return data
class User(Model): _name = "base.user" _key = ["login"] _name_field = "login" _string = "User" _fields = { "name": fields.Char("Name", required=True, search=True), "login": fields.Char("Login", required=True, search=True), "password": fields.Char("Password", password=True, size=256), "email": fields.Char("Email", search=True), "mobile": fields.Char("Mobile"), "role_id": fields.Many2One("role", "Role"), "profile_id": fields.Many2One("profile", "Profile", required=True, search=True), "lastlog": fields.DateTime("Last login"), "active": fields.Boolean("Active"), "comments": fields.One2Many("message", "related_id", "Comments"), "online_status": fields.Selection([["offline", "Offline"], ["online", "Online"]], "Status", function="get_online_status"), "contact_id": fields.Many2One("contact", "Contact"), "pin_code": fields.Char("PIN Code", password=True, size=256), "company_id": fields.Many2One("company", "Company", search=True), "company2_id": fields.Many2One("company", "Company #2", search=True), "password_reset_code": fields.Char("Password Reset Code"), } _order = "login" _defaults = { "activ_code": lambda *a: "%.x" % random.randint(0, 1 << 32), "active": True, } def name_search(self, name, condition=[], limit=None, context={}): cond = [[ "or", ["name", "ilike", "%" + name + "%"], ["login", "ilike", "%" + name + "%"] ], condition] ids = self.search(cond, limit=limit) return self.name_get(ids, context) def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids): name = "%s (%s)" % (obj.name, obj.login) vals.append([obj.id, name]) return vals def disable_users(self, context={}): max_users = self.get_max_users() if max_users is None: return db = database.get_connection() num_users = db.get("SELECT COUNT(*) FROM base_user WHERE active").count if num_users <= max_users: return res = db.get( "SELECT id FROM base_user WHERE active ORDER BY id OFFSET %d LIMIT 1" % max_users) user_id = res.id db.execute("UPDATE base_user SET active=false WHERE id>=%d" % user_id) def delete(self, ids, **kw): if 1 in ids: raise Exception("Can not delete root user (id=1)") super().delete(ids, **kw) def send_activ_email(self, ids, context={}): res = get_model("email.account").search([["type", "=", "smtp"]]) if not res: raise Exception("Email account not found") smtp_id = res[0] for user in self.browse(ids): from_addr = "*****@*****.**" to_addr = user.email subject = "Welcome to Netforce!" body = """Welcome to Netforce and thanks for signing up! Click on the link below to activate your account. http://nf1.netforce.com/action?name=nfw_activate&activ_code=%s""" % user.activ_code vals = { "type": "out", "account_id": smtp_id, "from_addr": from_addr, "to_addr": to_addr, "subject": subject, "body": body, } msg_id = get_model("email.message").create(vals) get_model("email.message").send([msg_id]) def send_password_reset_email(self, ids, context={}): res = get_model("email.account").search([["type", "=", "smtp"]]) if not res: raise Exception("Email account not found") smtp_id = res[0] for user in self.browse(ids): code = "%.x" % random.randint(0, 1 << 32) user.write({"reset_code": code}) from_addr = "*****@*****.**" to_addr = user.email subject = "Netforce password reset" body = """Click on the link below to reset your password. http://nf1.netforce.com/action?name=nfw_reset_passwd&reset_code=%s""" % code vals = { "type": "out", "account_id": smtp_id, "from_addr": from_addr, "to_addr": to_addr, "subject": subject, "body": body, } msg_id = get_model("email.message").create(vals) get_model("email.message").send([msg_id]) def get_online_status(self, ids, context={}): vals = {} db = database.get_connection() res = db.query("SELECT user_id FROM ws_listener") online_ids = set([r.user_id for r in res]) for obj in self.browse(ids): vals[obj.id] = obj.id in online_ids and "online" or "offline" return vals def check_password(self, login, password, context={}): db = database.get_connection() res = db.get("SELECT id,password FROM base_user WHERE login ILIKE %s", login) if not res: return None if not utils.check_password(password, res.password): return None return res.id def check_pin_code(self, ids, pin_code, context={}): user_id = ids[0] db = database.get_connection() res = db.get("SELECT pin_code FROM base_user WHERE id=%s", user_id) if not res: return None if not utils.check_password(pin_code, res.pin_code): return None return True def get_ui_params(self, context={}): user_id = access.get_active_user() if not user_id: return try: access.set_active_user(1) db = database.get_connection() if not db: return user = self.browse(user_id) params = { "name": user.name, } prof = user.profile_id params["default_model_perms"] = prof.default_model_perms params["model_perms"] = [] for p in prof.perms: params["model_perms"].append({ "model": p.model_id.name, "perm_read": p.perm_read, "perm_create": p.perm_create, "perm_write": p.perm_write, "perm_delete": p.perm_delete, }) params["field_perms"] = [] for p in prof.field_perms: params["field_perms"].append({ "model": p.field_id.model_id.name, "field": p.field_id.name, "perm_read": p.perm_read, "perm_write": p.perm_write, }) params["default_menu_access"] = prof.default_menu_access params["menu_perms"] = [] for p in prof.menu_perms: params["menu_perms"].append({ "action": p.action, "menu": p.menu, "access": p.access, }) params["other_perms"] = [p.code for p in prof.other_perms] return params finally: access.set_active_user(user_id) def password_reset(self, ids, context={}): obj = self.browse(ids) random_no = "%012d" % random.randint(0, 999999999999) obj.write({'password_reset_code': random_no}) return random_no
class Job(Model): _name = "job" _string = "Service Order" _name_field = "number" _audit_log = True _multi_company = True _fields = { "project_id": fields.Many2One("project", "Project", search=True), "contact_id": fields.Many2One("contact", "Customer", required=True, search=True), "template_id": fields.Many2One("job.template", "Template"), "service_type_id": fields.Many2One("service.type", "Service Type", search=True), "product_id": fields.Many2One("product", "Product"), # XXX: deprecated "name": fields.Char("Order Name", search=True), "number": fields.Char("Order Number", required=True, search=True), "description": fields.Text("Description"), "due_date": fields.Date("Due Date", search=True), "close_date": fields.Date("Close Date", search=True), "priority": fields.Selection([["low", "Low"], ["medium", "Medium"], ["high", "High"]], "Priority", search=True), "state": fields.Selection([["planned", "Planned"], ["allocated", "Allocated"], ["in_progress", "In Progress"], ["done", "Completed"], ["canceled", "Canceled"]], "Status", required=True), "overdue": fields.Boolean("Overdue", function="get_overdue", function_search="search_overdue"), "comments": fields.One2Many("message", "related_id", "Comments"), "documents": fields.One2Many("document", "related_id", "Documents"), "tasks": fields.One2Many("task", "job_id", "Tasks"), "days_late": fields.Integer("Days Late", function="get_days_late"), "user_id": fields.Many2One("base.user", "Assigned To"), # XXX: deprecated "resource_id": fields.Many2One("service.resource", "Assigned Resource", search=True), # XXX: deprecated "skill_level_id": fields.Many2One("skill.level", "Required Skill Level", search=True), "request_by_id": fields.Many2One("base.user", "Requested By", search=True), "user_board_id": fields.Boolean("User", store=False, function_search="search_user_board_id"), "sharing": fields.One2Many("share.record", "related_id", "Sharing"), "invoice_no": fields.Char("Invoice No."), # XXX: not used any more... "shared_board": fields.Boolean("Shared", store=False, function_search="search_shared_board"), "quotation_id": fields.Many2One("sale.quot", "Quotation"), "cancel_reason": fields.Text("Cancel Reason"), "cancel_periodic": fields.Boolean("Cancel Periodic"), "next_job_id": fields.Many2One("job", "Next Order"), "emails": fields.One2Many("email.message", "related_id", "Emails"), "company_id": fields.Many2One("company", "Company"), "invoices": fields.One2Many("account.invoice", "related_id", "Invoices"), "bill_amount": fields.Decimal("Billable Amount"), "invoice_id": fields.Many2One("account.invoice", "Invoice"), "is_duplicate": fields.Boolean("Duplicate"), "work_time": fields.One2Many("work.time", "job_id", "Work Time"), "pickings": fields.One2Many("stock.picking", "related_id", "Pickings"), "stock_moves": fields.One2Many("stock.move", "related_id", "Stock Movements"), "parts": fields.One2Many("job.part", "job_id", "Parts"), "other_costs": fields.One2Many("job.cost", "job_id", "Other Costs"), "items": fields.One2Many("job.item", "job_id", "Service Items"), "allocs": fields.One2Many("service.resource.alloc", "job_id", "Resource Allocations"), "time_start": fields.DateTime("Planned Start Time"), "time_stop": fields.DateTime("Planned Stop Time"), "location_id": fields.Many2One("stock.location", "Job Location"), "related_id": fields.Reference([["sale.order", "Sales Order"], ["rental.order","Rental Order"], ["issue", "Issue"]], "Related To"), "lines": fields.One2Many("job.line", "job_id", "Worksheet"), "complaints": fields.Text("Complaints"), "cause": fields.Text("Cause"), "correction": fields.Text("Correction"), "amount_total": fields.Decimal("Total Selling", function="get_total", function_multi=True), "amount_contract": fields.Decimal("Included In Contract", function="get_total", function_multi=True), "amount_job": fields.Decimal("Not Included In Contract", function="get_total", function_multi=True), "overdue": fields.Boolean("Overdue", function="get_overdue", function_search="search_overdue"), "date_open": fields.DateTime("Actual Start"), "date_close": fields.DateTime("Actual Stop"), "labor_cost": fields.Decimal("Labor Cost", function="get_cost", function_multi=True), "part_cost": fields.Decimal("Parts Cost", function="get_cost", function_multi=True), "other_cost": fields.Decimal("Other Cost", function="get_cost", function_multi=True), "total_cost": fields.Decimal("Total Cost", function="get_cost", function_multi=True), "labor_sell": fields.Decimal("Labor Selling", function="get_sell", function_multi=True), "part_sell": fields.Decimal("Parts Selling", function="get_sell", function_multi=True), "other_sell": fields.Decimal("Other Selling", function="get_sell", function_multi=True), "done_approved_by_id": fields.Many2One("base.user", "Approved By", readonly=True), "multi_visit_code_id": fields.Many2One("reason.code", "Multi Visit Reason Code", condition=[["type", "=", "service_multi_visit"]]), "late_response_code_id": fields.Many2One("reason.code", "Late Response Reason Code", condition=[["type", "=", "service_late_response"]]), "year": fields.Char("Year", sql_function=["year", "due_date"]), "quarter": fields.Char("Quarter", sql_function=["quarter", "due_date"]), "month": fields.Char("Month", sql_function=["month", "due_date"]), "week": fields.Char("Week", sql_function=["week", "due_date"]), "activities": fields.One2Many("activity","related_id","Activities"), "track_id": fields.Many2One("account.track.categ","Tracking Code"), "track_entries": fields.One2Many("account.track.entry",None,"Tracking Entries",function="get_track_entries",function_write="write_track_entries"), "track_balance": fields.Decimal("Tracking Balance",function="_get_related",function_context={"path":"track_id.balance"}), } _order = "number" _sql_constraints = [ ("number_uniq", "unique (number)", "The job number must be unique!"), ] def _get_number(self, context={}): while 1: num = get_model("sequence").get_number(type="job") 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(type="job") def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids): name = obj.number if obj.name: name += " - " + obj.name vals.append((obj.id, name)) return vals _defaults = { "state": "planned", "number": _get_number, "request_by_id": lambda *a: get_active_user(), #"company_id": lambda *a: get_active_company(), # XXX: don't use this yet "date_open": lambda *a: time.strftime("%Y-%m-%d"), } def write(self, ids, vals, **kw): if vals.get("state") == "done": vals["date_close"] = time.strftime("%Y-%m-%d") for obj in self.browse(ids): if not obj.done_approved_by_id: raise Exception("Service order has to be approved first") super().write(ids, vals, **kw) def get_total(self, ids, context={}): vals = {} for obj in self.browse(ids): amt_total = 0 amt_contract = 0 amt_job = 0 for line in obj.lines: amt_total += line.amount if line.payment_type == "contract": amt_contract += line.amount elif line.payment_type == "job": amt_job += line.amount vals[obj.id] = { "amount_total": amt_total, "amount_contract": amt_contract, "amount_job": amt_job, } return vals def onchange_template(self, context={}): data = context["data"] template_id = data["template_id"] tmpl = get_model("job.template").browse(template_id) data["service_type_id"] = tmpl.service_type_id.id data["description"] = tmpl.description data["skill_level_id"] = tmpl.skill_level_id.id data["lines"] = [] for line in tmpl.lines: line_vals = { "type": line.type, "product_id": line.product_id.id, "description": line.description, "qty": line.qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, } data["lines"].append(line_vals) return data def get_overdue(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.due_date: vals[obj.id] = obj.due_date < time.strftime( "%Y-%m-%d") and obj.state in ("planned", "allocated", "in_progress") else: vals[obj.id] = False return vals def search_overdue(self, clause, context={}): return [["due_date", "<", time.strftime("%Y-%m-%d")], ["state", "in", ["planned", "allocated", "in_progress"]]] def copy_to_pick_out(self, ids, context={}): obj = self.browse(ids)[0] vals = { "type": "out", "contact_id": obj.contact_id.id, "related_id": "job,%d" % obj.id, "lines": [], } res = get_model("stock.location").search([["type", "=", "customer"]]) if not res: raise Exception("Customer location not found") cust_loc_id = res[0] res = get_model("stock.location").search([["type", "=", "internal"]]) if not res: raise Exception("Warehouse location not found") wh_loc_id = res[0] for line in obj.lines: prod = line.product_id if prod.type not in ("stock", "consumable"): continue line_vals = { "product_id": prod.id, "qty": line.qty, "uom_id": line.uom_id.id, "location_from_id": prod.location_id.id or wh_loc_id, "location_to_id": obj.location_id.id or cust_loc_id, } vals["lines"].append(("create", line_vals)) if not vals["lines"]: raise Exception("Nothing to issue") new_id = get_model("stock.picking").create(vals, context={"pick_type": "out"}) pick = get_model("stock.picking").browse(new_id) return { "flash": "Goods issue %s copied from service order %s" % (pick.number, obj.number), "next": { "name": "pick_out", "mode": "form", "active_id": new_id, } } def copy_to_invoice(self, ids, context={}): obj = self.browse(ids)[0] inv_vals = { "type": "out", "inv_type": "invoice", "ref": obj.number, "related_id": "job,%s" % obj.id, "contact_id": obj.contact_id.id, "lines": [], } for line in obj.lines: if line.payment_type != "job": continue prod = line.product_id line_vals = { "product_id": prod.id, "description": line.description, "qty": line.qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, "account_id": prod.sale_account_id.id if prod else None, "tax_id": prod.sale_tax_id.id if prod else None, "amount": line.amount, } inv_vals["lines"].append(("create", line_vals)) if not inv_vals["lines"]: raise Exception("Nothing to invoice") inv_id = get_model("account.invoice").create(inv_vals, {"type": "out", "inv_type": "invoice"}) inv = get_model("account.invoice").browse(inv_id) return { "next": { "name": "view_invoice", "active_id": inv_id, }, "flash": "Invoice %s created from job %s" % (inv.number, obj.number), } def onchange_product(self, context={}): data = context["data"] path = context["path"] line = get_data_path(data, path, parent=True) prod_id = line["product_id"] prod = get_model("product").browse(prod_id) line["uom_id"] = prod.uom_id.id line["unit_price"] = prod.sale_price line["description"] = prod.description return data def onchange_due_date(self, context={}): print("onchange_due_date") data = context["data"] data['time_start'] = data['due_date'] return data def onchange_close_date(self, context={}): print("onchange_close_date") data = context["data"] crr_date = time.strftime("%Y-%m-%d") close_date = data['close_date'] due_date = data['due_date'] if crr_date >= close_date: data['state'] = 'done' elif crr_date >= due_date and crr_date <= close_date: data['state'] = 'in_progress' return data def get_cost(self, ids, context={}): vals = {} for obj in self.browse(ids): labor_cost = 0 for time in obj.work_time: labor_cost += time.amount or 0 other_cost = 0 for line in obj.lines: if line.type != "other": continue prod = line.product_id other_cost += prod.cost_price or 0 job_loc_id = obj.location_id.id if not job_loc_id: res = get_model("stock.location").search([["type", "=", "customer"]]) if res: job_loc_id = res[0] part_cost = 0 for pick in obj.pickings: for move in pick.lines: amt = move.qty * (move.unit_price or 0) if move.location_to_id.id == job_loc_id and move.location_from_id.id != job_loc_id: part_cost += amt elif move.location_from_id.id == job_loc_id and move.location_to_id.id != job_loc_id: part_cost -= amt vals[obj.id] = { "labor_cost": labor_cost, "part_cost": part_cost, "other_cost": other_cost, "total_cost": labor_cost + part_cost + other_cost, } return vals def get_sell(self, ids, context={}): vals = {} for obj in self.browse(ids): labor_sell = 0 other_sell = 0 part_sell = 0 for line in obj.lines: if line.type == "labor": labor_sell += line.amount elif line.type == "part": part_sell += line.amount elif line.type == "other": other_sell += line.amount vals[obj.id] = { "labor_sell": labor_sell, "part_sell": part_sell, "other_sell": other_sell, } return vals def approve_done(self, ids, context={}): if not check_permission_other("job_approve_done"): raise Exception("Permission denied") obj = self.browse(ids)[0] user_id = get_active_user() obj.write({"done_approved_by_id": user_id}) return { "next": { "name": "job", "mode": "form", "active_id": obj.id, }, "flash": "Service order completion approved successfully", } def get_days_late(self, ids, context={}): vals = {} d = datetime.now() for obj in self.browse(ids): if obj.due_date: vals[obj.id] = (d - datetime.strptime(obj.due_date, "%Y-%m-%d")).days else: vals[obj.id] = None return vals def get_track_entries(self,ids,context={}): vals={} for obj in self.browse(ids): if not obj.track_id: vals[obj.id]=[] continue res=get_model("account.track.entry").search([["track_id","child_of",obj.track_id.id]]) vals[obj.id]=res return vals def write_track_entries(self,ids,field,val,context={}): for op in val: if op[0]=="create": rel_vals=op[1] for obj in self.browse(ids): if not obj.track_id: continue rel_vals["track_id"]=obj.track_id.id get_model("account.track.entry").create(rel_vals,context=context) elif op[0]=="write": rel_ids=op[1] rel_vals=op[2] get_model("account.track.entry").write(rel_ids,rel_vals,context=context) elif op[0]=="delete": rel_ids=op[1] get_model("account.track.entry").delete(rel_ids,context=context) def create_track(self,ids,context={}): obj=self.browse(ids[0]) code=obj.number res=get_model("account.track.categ").search([["code","=",code]]) if res: track_id=res[0] else: parent_id=obj.project_id.track_id.id if obj.project_id else None track_id=get_model("account.track.categ").create({ "code": code, "name": code, "type": "1", "parent_id": parent_id, }) obj.write({"track_id": track_id})
class Move(Model): _name = "stock.move" _string = "Stock Movement" _name_field = "number" _multi_company = True _key = ["company_id", "number"] _fields = { "ref": fields.Char("Ref", search=True), # XXX: deprecated "product_id": fields.Many2One("product", "Product", required=True, search=True), "location_from_id": fields.Many2One("stock.location", "From Location", required=True, search=True), "location_to_id": fields.Many2One("stock.location", "To Location", required=True, search=True), "qty": fields.Decimal("Qty", required=True, scale=6), "uom_id": fields.Many2One("uom", "UoM", required=True), "picking_id": fields.Many2One("stock.picking", "Picking", on_delete="cascade"), "date": fields.DateTime("Date", required=True, search=True), "cost_price_cur": fields.Decimal("Cost Price (Cur)", scale=6), # in picking currency "cost_price": fields.Decimal("Cost Price", scale=6), # in company currency "cost_amount": fields.Decimal("Cost Amount"), # in company currency "cost_fixed": fields.Boolean("Cost Fixed"), # don't calculate cost "state": fields.Selection([("draft", "Draft"), ("pending", "Planned"), ("approved", "Approved"), ("done", "Completed"), ("voided", "Voided")], "Status", required=True), "stock_count_id": fields.Many2One("stock.count", "Stock Count"), "move_id": fields.Many2One("account.move", "Journal Entry"), "user_id": fields.Many2One("base.user", "User"), "contact_id": fields.Many2One("contact", "Contact"), "comments": fields.One2Many("message", "related_id", "Comments"), "serial_no": fields.Char("Serial No.", search=True), # XXX: deprecated "lot_id": fields.Many2One("stock.lot", "Lot / Serial Number"), "container_from_id": fields.Many2One("stock.container", "From Container"), "container_to_id": fields.Many2One("stock.container", "To Container"), "packaging_id": fields.Many2One("stock.packaging", "Packaging"), "num_packages": fields.Integer("# Packages"), "notes": fields.Text("Notes"), "qty2": fields.Decimal("Qty2"), "company_id": fields.Many2One("company", "Company"), "invoice_id": fields.Many2One("account.invoice", "Invoice"), "related_id": fields.Reference( [["sale.order", "Sales Order"], ["purchase.order", "Purchase Order"], ["job", "Service Order"], ["account.invoice", "Invoice"], ["pawn.loan", "Loan"]], "Related To"), "number": fields.Char("Number", required=True, search=True), "journal_id": fields.Many2One("stock.journal", "Journal", required=True, search=True), "alloc_costs": fields.One2Many("landed.cost.alloc", "move_id", "Allocated Costs"), "alloc_cost_amount": fields.Decimal("Allocated Costs", scale=6, function="get_alloc_cost_amount"), "track_id": fields.Many2One("account.track.categ", "Track"), "parent_id": fields.Many2One("stock.move", "Parent"), } _order = "date desc,id desc" def _get_loc_from(self, context={}): print("_get_loc_from", context) data = context.get("data") settings = get_model("settings").browse(1) if data: journal_id = data.get("journal_id") if journal_id: journal = get_model("stock.journal").browse(journal_id) if journal.location_from_id: return journal.location_from_id.id pick_type = context.get("pick_type") if pick_type == "in": journal = settings.pick_in_journal_id elif pick_type == "out": journal = settings.pick_out_journal_id elif pick_type == "internal": journal = settings.pick_internal_journal_id else: journal = None if journal and journal.location_from_id: return journal.location_from_id.id if pick_type != "in": return None res = get_model("stock.location").search([["type", "=", "supplier"]], order="id") if not res: return None return res[0] def _get_loc_to(self, context={}): print("_get_loc_to", context) data = context.get("data") settings = get_model("settings").browse(1) if data: journal_id = data.get("journal_id") if journal_id: journal = get_model("stock.journal").browse(journal_id) if journal.location_to_id: return journal.location_to_id.id pick_type = context.get("pick_type") pick_type = context.get("pick_type") if pick_type == "in": journal = settings.pick_in_journal_id elif pick_type == "out": journal = settings.pick_out_journal_id elif pick_type == "internal": journal = settings.pick_internal_journal_id else: journal = None if journal and journal.location_from_id: return journal.location_to_id.id if pick_type != "out": return None res = get_model("stock.location").search([["type", "=", "customer"]]) if not res: return None return res[0] def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence("stock_move") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id) res = self.search([["number", "=", num]]) if not res: return num get_model("sequence").increment_number(seq_id) _defaults = { "state": "draft", "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "location_from_id": _get_loc_from, "location_to_id": _get_loc_to, "cost_price": 0, "cost_amount": 0, "company_id": lambda *a: get_active_company(), "number": _get_number, } def create(self, vals, context={}): pick_id = vals.get("picking_id") if pick_id: pick = get_model("stock.picking").browse(pick_id) vals["date"] = pick.date vals["picking_id"] = pick.id vals["journal_id"] = pick.journal_id.id new_id = super().create(vals, context=context) self.function_store([new_id]) prod_id = vals["product_id"] user_id = get_active_user() set_active_user(1) get_model("product").write([prod_id], {"update_balance": True}) set_active_user(user_id) return new_id def write(self, ids, vals, context={}): prod_ids = [] for obj in self.browse(ids): prod_ids.append(obj.product_id.id) super().write(ids, vals, context=context) prod_id = vals.get("product_id") if prod_id: prod_ids.append(prod_id) self.function_store(ids) user_id = get_active_user() set_active_user(1) get_model("product").write(prod_ids, {"update_balance": True}) set_active_user(user_id) def delete(self, ids, **kw): prod_ids = [] for obj in self.browse(ids): prod_ids.append(obj.product_id.id) super().delete(ids, **kw) user_id = get_active_user() set_active_user(1) get_model("product").write(prod_ids, {"update_balance": True}) set_active_user(user_id) def view_stock_transaction(self, ids, context={}): obj = self.browse(ids[0]) if obj.picking_id: pick = obj.picking_id if pick.type == "in": next = { "name": "pick_in", "mode": "form", "active_id": pick.id, } elif pick.type == "out": next = { "name": "pick_out", "mode": "form", "active_id": pick.id, } elif pick.type == "internal": next = { "name": "pick_internal", "mode": "form", "active_id": pick.id, } elif obj.stock_count_id: next = { "name": "stock_count", "mode": "form", "active_id": obj.stock_count_id.id, } else: raise Exception("Invalid stock move") return {"next": next} def set_done(self, ids, context={}): print("stock_move.set_done", ids) settings = get_model("settings").browse(1) prod_ids = [] self.write(ids, {"state": "done"}, context=context) for obj in self.browse(ids): prod = obj.product_id prod_ids.append(prod.id) pick = obj.picking_id vals = {} if not obj.qty2 and prod.qty2_factor: qty2 = get_model("uom").convert( obj.qty, obj.uom_id.id, prod.uom_id.id) * prod.qty2_factor vals["qty2"] = qty2 elif prod.require_qty2 and obj.qty2 is None: raise Exception("Missing secondary qty for product %s" % prod.code) if pick and pick.related_id and not obj.related_id: vals["related_id"] = "%s,%d" % (pick.related_id._model, pick.related_id.id) if pick and not pick.related_id and not obj.related_id: vals["related_id"] = "%s,%d" % (pick._model, pick.id) if obj.location_from_id.type == "view": raise Exception("Source location '%s' is a view location" % obj.location_from_id.name) if obj.location_to_id.type == "view": raise Exception( "Destination location '%s' is a view location" % obj.location_to_id.name) if prod.require_lot and not obj.lot_id: raise Exception("Missing lot for product %s" % prod.code) if vals: obj.write(vals=vals, context=context) # change state in borrow requests # XXX: remove this if not obj.related_id: if pick.related_id._model == "product.borrow": if pick.related_id.is_return_item: pick.related_id.write({"state": "done"}) elif obj.related_id._model == "product.borrow": if obj.related_id.is_return_item: obj.related_id.write({"state": "done"}) prod_ids = list(set(prod_ids)) if prod_ids and settings.stock_cost_auto_compute: get_model("stock.compute.cost").compute_cost( [], context={"product_ids": prod_ids}) if settings.stock_cost_mode == "perpetual" and not context.get( "no_post"): self.post(ids, context=context) self.update_lots(ids, context=context) self.set_reference(ids, context=context) self.check_periods(ids, context=context) print("<<< stock_move.set_done") def check_periods(self, ids, context={}): for obj in self.browse(ids): d = obj.date[:10] res = get_model("stock.period").search([["date_from", "<=", d], ["date_to", ">=", d], ["state", "=", "posted"]]) if res: raise Exception( "Failed to validate stock movement because stock period already posted" ) def set_reference(self, ids, context={}): for obj in self.browse(ids): if obj.ref or not obj.related_id: continue ref = obj.related_id.name_get()[0][1] obj.write({"ref": ref}) def reverse(self, ids, context={}): move_ids = [] for obj in self.browse(ids): if obj.state != "done": raise Exception( "Failed to reverse stock movement: invalid state") vals = { "journal_id": obj.journal_id.id, "product_id": obj.product_id.id, "qty": obj.qty, "uom_id": obj.uom_id.id, "location_from_id": obj.location_to_id.id, "location_to_id": obj.location_from_id.id, "cost_price_cur": obj.cost_price_cur, "cost_price": obj.cost_price, "cost_amount": obj.cost_amount, "qty2": obj.qty2, "ref": "Reverse: %s" % obj.ref if obj.ref else None, "related_id": "%s,%s" % (obj.related_id._model, obj.related_id.id) if obj.related_id else None, "picking_id": obj.picking_id.id, } move_id = self.create(vals) move_ids.append(move_id) self.set_done(move_ids) def get_alloc_cost_amount(self, ids, context={}): vals = {} for obj in self.browse(ids): amt = 0 for alloc in obj.alloc_costs: if alloc.landed_id.state != "posted": continue amt += alloc.amount or 0 vals[obj.id] = amt return vals def post(self, ids, context={}): print("stock.move post", ids) accounts = {} post_date = None pick_ids = [] for move in self.browse(ids): if move.move_id: raise Exception( "Journal entry already create for stock movement %s" % move.number) date = move.date[:10] if post_date is None: post_date = date else: if date != post_date: raise Exception( "Failed to post stock movements because they have different dates" ) prod = move.product_id #desc="[%s] %s @ %s %s "%(prod.code,prod.name,round(move.qty,2),move.uom_id.name) # XXX: too many lines in JE desc = "Inventory costing" acc_from_id = move.location_from_id.account_id.id if move.location_from_id.type == "customer": if prod.cogs_account_id: acc_from_id = prod.cogs_account_id.id elif prod.categ_id and prod.categ_id.cogs_account_id: acc_from_id = prod.categ_id.cogs_account_id.id elif move.location_from_id.type == "internal": if prod.stock_account_id: acc_from_id = prod.stock_account_id.id elif prod.categ_id and prod.categ_id.stock_account_id: acc_from_id = prod.categ_id.stock_account_id.id if not acc_from_id: raise Exception( "Missing input account for stock movement %s (date=%s, ref=%s, product=%s)" % (move.id, move.date, move.ref, prod.name)) acc_to_id = move.location_to_id.account_id.id if move.location_to_id.type == "customer": if prod.cogs_account_id: acc_to_id = prod.cogs_account_id.id elif prod.categ_id and prod.categ_id.cogs_account_id: acc_to_id = prod.categ_id.cogs_account_id.id elif move.location_to_id.type == "internal": if prod.stock_account_id: acc_to_id = prod.stock_account_id.id elif prod.categ_id and prod.categ_id.stock_account_id: acc_to_id = prod.categ_id.stock_account_id.id if not acc_to_id: raise Exception( "Missing output account for stock movement %s (date=%s, ref=%s, product=%s)" % (move.id, move.date, move.ref, prod.name)) track_from_id = move.location_from_id.track_id.id track_to_id = move.track_id.id or move.location_to_id.track_id.id # XXX amt = move.cost_amount or 0 if not move.move_id: # XXX: avoid create double journal entry for LC for ex accounts.setdefault((acc_from_id, track_from_id, desc), 0) accounts.setdefault((acc_to_id, track_to_id, desc), 0) accounts[(acc_from_id, track_from_id, desc)] -= amt accounts[(acc_to_id, track_to_id, desc)] += amt if move.picking_id: pick_ids.append(move.picking_id.id) lines = [] for (acc_id, track_id, desc), amt in accounts.items(): lines.append({ "description": desc, "account_id": acc_id, "track_id": track_id, "debit": amt > 0 and amt or 0, "credit": amt < 0 and -amt or 0, }) vals = { "narration": "Inventory costing", "date": post_date, "lines": [("create", vals) for vals in lines], } pick_ids = list(set(pick_ids)) if len(pick_ids) == 1: vals["related_id"] = "stock.picking,%s" % pick_ids[0] move_id = get_model("account.move").create(vals) get_model("account.move").post([move_id]) get_model("stock.move").write(ids, {"move_id": move_id}) return move_id def to_draft(self, ids, context={}): move_ids = [] for obj in self.browse(ids): if obj.move_id: move_ids.append(obj.move_id.id) move_ids = list(set(move_ids)) for move in get_model("account.move").browse(move_ids): move.void() move.delete() self.write(ids, {"state": "draft"}) # change state in borrow requests for obj in self.browse(ids): if obj.related_id._model == "product.borrow": if not obj.related_id.is_return_item: obj.related_id.write({"state": "approved"}) def update_lots(self, ids, context={}): for obj in self.browse(ids): lot = obj.lot_id if not lot: continue if obj.location_from_id.type != "internal" and obj.location_to_id.type == "internal": lot.write({"received_date": obj.date}) # XXX def get_unit_price(self, ids, context={}): settings = get_model("settings").browse(1) vals = {} for obj in self.browse(ids): pick = obj.picking_id if pick: if pick.currency_rate: currency_rate = pick.currency_rate else: if pick.currency_id.id == settings.currency_id.id: currency_rate = 1 else: rate_from = pick.currency_id.get_rate(date=pick.date) if not rate_from: raise Exception("Missing currency rate for %s" % pick.currency_id.code) rate_to = settings.currency_id.get_rate(date=pick.date) if not rate_to: raise Exception("Missing currency rate for %s" % settings.currency_id.code) currency_rate = rate_from / rate_to price = obj.unit_price_cur or 0 price_conv = get_model("currency").convert( price, pick.currency_id.id, settings.currency_id.id, rate=currency_rate) else: price_conv = None vals[obj.id] = price_conv return vals
class LandedCostAlloc(Model): _name = "landed.cost.alloc" _fields = { "landed_id": fields.Many2One("landed.cost", "Landed Cost", required=True, on_delete="cascade"), "move_id": fields.Many2One("stock.move", "Stock Movement", required=True), "date": fields.DateTime("Date", function="_get_related", function_context={"path": "move_id.date"}), "picking_id": fields.Many2One("stock.picking", "Goods Receipt", function="_get_related", function_context={"path": "move_id.picking_id"}), "contact_id": fields.Many2One( "contact", "Contact", function="_get_related", function_context={"path": "move_id.picking_id.contact_id"}), "product_id": fields.Many2One("product", "Product", function="_get_related", function_context={"path": "move_id.product_id"}), "qty": fields.Decimal("Qty", function="_get_related", function_context={"path": "move_id.qty"}), "uom_id": fields.Many2One("uom", "UoM", function="_get_related", function_context={"path": "move_id.uom_id"}), "cost_price": fields.Decimal("Base Cost Price", function="_get_related", function_context={"path": "move_id.cost_price"}), "cost_amount": fields.Decimal("Base Cost Amount", function="_get_related", function_context={"path": "move_id.cost_amount"}), "location_from_id": fields.Many2One("stock.location", "From Location", function="_get_related", function_context={"path": "move_id.location_from_id"}), "location_to_id": fields.Many2One("stock.location", "To Location", function="_get_related", function_context={"path": "move_id.location_to_id"}), "track_id": fields.Many2One("account.track.categ", "Track", function="_get_related", function_context={"path": "move_id.track_id"}), "qty_stock_gr": fields.Decimal("Qty In Stock GR", function="_get_qty_stock", function_multi=True), "qty_stock_lc": fields.Decimal("Qty In Stock LC", function="_get_qty_stock", function_multi=True), "est_ship": fields.Decimal("Est. Shipping"), "est_duty": fields.Decimal("Est. Duty"), "act_ship": fields.Decimal("Act. Shipping"), "act_duty": fields.Decimal("Act. Duty"), "amount": fields.Decimal("Total Alloc. Cost", function="_get_total", function_multi=True), "percent": fields.Decimal("Cost Percent", function="_get_total", function_multi=True), } def _get_qty_stock(self, ids, context={}): gr_keys = {} lc_keys = {} for obj in self.browse(ids): move = obj.move_id pick_id = move.picking_id.id lc_id = obj.landed_id.id k = (move.product_id.id, move.lot_id.id, move.location_to_id.id, move.container_to_id.id) gr_keys.setdefault(pick_id, []).append(k) lc_keys.setdefault(lc_id, []).append(k) gr_bals = {} for pick_id, keys in gr_keys.items(): pick = get_model("stock.picking").browse(pick_id) gr_bals[pick_id] = get_model("stock.balance").compute_key_balances( keys, context={"date_to": pick.date}) lc_bals = {} for lc_id, keys in lc_keys.items(): lc = get_model("landed.cost").browse(lc_id) lc_bals[lc_id] = get_model("stock.balance").compute_key_balances( keys, context={"date_to": lc.date}) print("gr_bals", gr_bals) print("lc_bals", lc_bals) vals = {} for obj in self.browse(ids): move = obj.move_id pick_id = move.picking_id.id lc_id = obj.landed_id.id k = (move.product_id.id, move.lot_id.id, move.location_to_id.id, move.container_to_id.id) qty_gr = gr_bals[pick_id][k][0] qty_lc = lc_bals[lc_id][k][0] vals[obj.id] = { "qty_stock_gr": qty_gr, "qty_stock_lc": qty_lc, } return vals def _get_total(self, ids, context={}): vals = {} for obj in self.browse(ids): amt = 0 amt += obj.est_ship or 0 amt += obj.est_duty or 0 amt += obj.act_ship or 0 amt += obj.act_duty or 0 vals[obj.id] = { "amount": amt, "percent": amt * 100 / obj.cost_amount if obj.cost_amount else None, } return vals
class Product(Model): _name = "product" _string = "Product" _audit_log = True _key = ["code", "state", "company_id"] _order = "code,name" _export_name_field = "code" _history = True _fields = { "name": fields.Char("Name", required=True, search=True, translate=True, size=256), "code": fields.Char("Code", required=True, search=True), "type": fields.Selection([["stock", "Stockable"], ["consumable", "Consumable"], ["service", "Service"], ["master", "Master"], ["bundle", "Bundle"]], "Product Type", required=True, search=True), "uom_id": fields.Many2One("uom", "Default UoM", required=True, search=True), "parent_id": fields.Many2One("product", "Master Product"), "categ_id": fields.Many2One("product.categ", "Product Category", search=True), "description": fields.Text("Description", translate=True), "purchase_price": fields.Decimal("Purchase Price", scale=6), "sale_price": fields.Decimal("List Price (Sales Invoice UoM)", scale=6), "sale_price_order_uom": fields.Decimal("List Price (Sales Order UoM)", scale=6, function="get_sale_price_order_uom"), "tags": fields.Many2Many("tag", "Tags"), "image": fields.File("Image"), "cost_method": fields.Selection( [["standard", "Standard Cost"], ["average", "Weighted Average"], ["fifo", "FIFO"], ["lifo", "LIFO"]], "Costing Method"), "cost_price": fields.Decimal("Cost Price", scale=6), "stock_in_account_id": fields.Many2One("account.account", "Stock Input Account", multi_company=True), # XXX: deprecated "stock_out_account_id": fields.Many2One("account.account", "Stock Output Account", multi_company=True), # XXX: deprecated "cogs_account_id": fields.Many2One("account.account", "Cost Of Goods Sold Account", multi_company=True), "stock_account_id": fields.Many2One("account.account", "Inventory Account", multi_company=True), "purchase_account_id": fields.Many2One("account.account", "Purchase Account", multi_company=True), "purchase_tax_id": fields.Many2One("account.tax.rate", "Purchase Tax"), "supplier_id": fields.Many2One("contact", "Default Supplier"), # XXX: deprecated "sale_account_id": fields.Many2One("account.account", "Sales Account", multi_company=True), "sale_return_account_id": fields.Many2One("account.account", "Sales Returns Account", multi_company=True), "sale_promotion_account_id": fields.Many2One("account.account", "Sales Promotion Account", multi_company=True), "sale_tax_id": fields.Many2One("account.tax.rate", "Sales Tax"), "sale_promotion_tax_id": fields.Many2One("account.tax.rate", "Promotion Tax"), "location_id": fields.Many2One("stock.location", "Warehouse"), # XXX: deprecated "bin_location": fields.Char("Bin Location"), "update_balance": fields.Boolean("Update Balance"), "active": fields.Boolean("Active"), "comments": fields.One2Many("message", "related_id", "Comments"), "categs": fields.Many2Many("product.categ", "Other Categories"), # XXX: deprecated "attributes": fields.One2Many("product.attribute.value", "product_id", "Attributes"), "variants": fields.One2Many("product", "parent_id", "Variants"), #"variant_values": fields.One2Many("product.custom.option.variant.value","product_id","Variant Values"), "custom_options": fields.Many2Many("product.custom.option", "Custom Options"), "images": fields.One2Many("product.image", "product_id", "Images"), "store_type_id": fields.Many2One("store.type", "Storage Type"), "brand_id": fields.Many2One("product.brand", "Brand", search=True), "related_products": fields.Many2Many("product", "Related Products", relfield="product1_id", relfield_other="product2_id"), "min_sale_qty": fields.Decimal("Min Sale Qty"), "sale_unit_qty": fields.Decimal("Sale Unit Qty"), "shelf_life": fields.Decimal("Shelf Life (Days)"), "weight": fields.Decimal("Weight (Kg)"), "volume": fields.Decimal("Volume (M^3)"), "width": fields.Decimal("Width"), "height": fields.Decimal("Height"), "length": fields.Decimal("Length"), "packing_size": fields.Char("Packing Size"), "details": fields.Text("Product Details", translate=True), "details2": fields.Text("Product Details (2)", translate=True), "details2_label": fields.Char("Product Details Label (2)"), "details3": fields.Text("Product Details (3)", translate=True), "details3_label": fields.Char("Product Details Label (3)"), "other_url": fields.Char("Product URL", size=256), "purchase_currency_id": fields.Many2One("currency", "Purchase Currency"), "purchase_currency_rate": fields.Decimal("Purchase Currency Rate", scale=6), "purchase_duty_percent": fields.Decimal("Import Duty (%)"), "purchase_ship_percent": fields.Decimal("Shipping Charge (%)"), "landed_cost": fields.Decimal("Landed Cost", function="get_landed_cost", function_multi=True), "landed_cost_conv": fields.Decimal("Landed Cost (Conv)", function="get_landed_cost", function_multi=True), "gross_profit": fields.Decimal("Gross Profit (%)"), "auto_list_price": fields.Decimal("Auto List Price", function="get_auto_list_price"), "max_discount": fields.Decimal("Max Discount (%)", function="get_max_discount"), "price_index": fields.Decimal("Price Index", function="get_price_index"), "price_notes": fields.Text("Notes"), "price_date": fields.Date("Price Date"), "pricelist_items": fields.One2Many("price.list.item", "product_id", "Pricelist Items"), "procure_method": fields.Selection([["mto", "Make To Order"], ["mts", "Make To Stock"]], "Procurement Method"), "supply_method": fields.Selection( [["purchase", "Purchase"], ["production", "Production"]], "Supply Method"), "can_sell": fields.Boolean("Can Sell"), "can_purchase": fields.Boolean("Can Purchase"), "id": fields.Integer("Database ID", readonly=True), # MTS "create_time": fields.DateTime("Create Time", readonly=True), # MTS "supplier_product_code": fields.Char("Supplier Product Code"), # XXX: deprecated "require_qty2": fields.Boolean("Require Secondary Qty"), "qty2_factor": fields.Decimal("UoM -> Secondary Qty Factor", scale=6), "replacements": fields.Many2Many("product", "Replacement Products", reltable="m2m_product_replacement", relfield="product1_id", relfield_other="product2_id"), "suppliers": fields.One2Many("product.supplier", "product_id", "Suppliers"), "max_qty_loss": fields.Decimal("Max Qty Loss"), "documents": fields.One2Many("document", "related_id", "Documents"), "ship_methods": fields.Many2Many("ship.method", "Shipping Methods"), "ecom_discount_percent": fields.Decimal("Ecom. Discount Percent"), # XXX: deprecated "ecom_special_price": fields.Decimal("Ecom. Special Price"), # XXX: deprecated #"ecom_sale_price": fields.Decimal("Ecom. Sale Price", function="get_ecom_sale_price", function_multi=True), # XXX: deprecated #"ecom_has_discount": fields.Decimal("Ecom. Has Discount", function="get_ecom_sale_price", function_multi=True), # XXX: deprecated "variant_attributes": fields.Json("Variant Attributes", function="get_variant_attributes"), "company_id": fields.Many2One("company", "Company"), "sale_channels": fields.Many2Many("sale.channel", "Sales Channels"), "customer_price": fields.Decimal("Customer Price", function="get_customer_price", function_multi=True), "customer_has_discount": fields.Decimal("Customer Has Discount", function="get_customer_price", function_multi=True), "customer_discount_text": fields.Decimal("Customer Discount Text", function="get_customer_price", function_multi=True), "customer_discount_percent": fields.Decimal("Customer Discount Percent", function="get_customer_price", function_multi=True), "sale_company_id": fields.Many2One("company", "Sold By"), "groups": fields.Many2Many("product.group", "Groups"), "payment_methods": fields.Many2Many("payment.method", "Payment Methods"), "locations": fields.One2Many("product.location", "product_id", "Warehouses"), "stock_qty": fields.Decimal("Total Stock Qty", function="get_stock_qty"), "stock_lots": fields.Many2Many("stock.lot", "Lots In Stock", function="get_stock_lots"), "state": fields.Selection([["draft", "Draft"], ["approved", "Approved"]], "Status"), "sale_uom_id": fields.Many2One("uom", "Sales Order UoM"), "sale_invoice_uom_id": fields.Many2One("uom", "Sales Invoice UoM"), "sale_to_stock_uom_factor": fields.Decimal("Sales Order -> Stock Uom Conversion Factor", scale=6), "sale_to_invoice_uom_factor": fields.Decimal("Sales Order -> Sales Invoice Uom Conversion Factor", scale=6), "purchase_uom_id": fields.Many2One("uom", "Purchase Order UoM"), "purchase_invoice_uom_id": fields.Many2One("uom", "Purchase Invoice UoM"), "purchase_to_stock_uom_factor": fields.Decimal("Purchase Order -> Stock Uom Conversion Factor", scale=6), "purchase_to_invoice_uom_factor": fields.Decimal( "Purchase Order -> Purchase Invoice Uom Conversion Factor", scale=6), "purchase_lead_time": fields.Integer("Purchasing Lead Time (Days)"), "purchase_min_qty": fields.Decimal("Purchase Minimum Qty"), "purchase_qty_multiple": fields.Decimal("Purchase Qty Multiple"), "mfg_lead_time": fields.Integer("Manufacturing Lead Time (Days)"), "mfg_min_qty": fields.Decimal("Manufacturing Minimum Qty"), "mfg_qty_multiple": fields.Decimal("Manufacturing Qty Multiple"), #"purchase_price_uom_id": fields.Many2One("uom", "Purchase Price UoM"), # not needed? #"sale_price_uom_id": fields.Many2One("uom", "List Price UoM"), # not needed? "events": fields.Many2Many("sale.event", "Events"), "is_published": fields.Boolean("Publish Product"), "require_lot": fields.Boolean("Require Lot"), "lot_select": fields.Selection([["fifo", "FIFO"], ["fefo", "FEFO"]], "Lot Selection"), "components": fields.One2Many("product.component", "product_id", "Bundle Components"), "approve_date": fields.DateTime("Approve Date"), "service_items": fields.One2Many("service.item", "product_id", "Service Items"), "lots": fields.One2Many("stock.lot", "product_id", "Lots"), "stock_plan_horizon": fields.Integer("Inventory Planning Horizon (days)"), # XXX: deprecated "ecom_hide_qty": fields.Boolean("Hide Stock Qty From Website"), "ecom_hide_unavail": fields.Boolean("Hide From Website When Out Of Stock"), "ecom_no_order_unavail": fields.Boolean("Prevent Orders When Out Of Stock"), "ecom_select_lot": fields.Boolean("Customers Can Select Lot When Ordering"), "product_origin": fields.Char("Product Origin"), "stock_balances": fields.One2Many("stock.balance", "product_id", "Stock Balances"), "check_lot_neg_stock": fields.Boolean("Check Lot Negative Stock"), "sale_lead_time_nostock": fields.Integer("Sale Lead Time When Out Of Stock (Days)"), "ship_methods": fields.Many2Many("ship.method", "Shipping Methods"), "delivery_weekdays": fields.Char("Delivery Weekday Constraints"), } _defaults = { "update_balance": True, "active": True, "can_sell": False, "can_purchase": False, "company_id": lambda *a: access.get_active_company(), "state": "draft", } def name_get(self, ids, context={}): vals = [] for obj in self.browse(ids): if obj.code: name = "[%s] %s" % (obj.code, obj.name) else: name = obj.name vals.append((obj.id, name, obj.image)) return vals def name_search(self, name, condition=None, context={}, limit=None, **kw): print("condition", condition) search_mode = context.get("search_mode") print("##############################") print("search_mode", search_mode) if search_mode == "suffix": cond = [["code", "=ilike", "%" + name]] elif search_mode == "prefix": cond = [["code", "=ilike", name + "%"]] else: cond = [["code", "ilike", name]] if condition: cond = [cond, condition] ids1 = self.search(cond, limit=limit) if search_mode == "suffix": cond = [["name", "=ilike", "%" + name]] elif search_mode == "prefix": cond = [["name", "=ilike", name + "%"]] else: cond = [["name", "ilike", name]] if condition: cond = [cond, condition] ids2 = self.search(cond, limit=limit) ids = list(set(ids1 + ids2)) return self.name_get(ids, context=context) def copy(self, ids, context={}): replace_id = context.get("replace_id") obj = self.browse(ids)[0] code = obj.code if not replace_id: for i in range(1, 10): code = obj.code + " (%s)" % i res = self.search([["code", "=", code]]) if not res: break vals = { "name": obj.name, "code": code, "type": obj.type, "uom_id": obj.uom_id.id, #"parent_id": obj.parent_id.id, XXX "description": obj.description, "image": obj.image, "categ_id": obj.categ_id.id, "categs": [("set", [c.id for c in obj.categs])], "supply_method": obj.supply_method, "procure_method": obj.procure_method, "can_sell": obj.can_sell, "can_purchase": obj.can_purchase, "sale_uom_id": obj.sale_uom_id.id, "sale_invoice_uom_id": obj.sale_invoice_uom_id.id, "sale_to_stock_uom_factor": obj.sale_to_stock_uom_factor, "sale_to_invoice_uom_factor": obj.sale_to_invoice_uom_factor, "purchase_uom_id": obj.purchase_uom_id.id, "purchase_invoice_uom_id": obj.purchase_invoice_uom_id.id, "purchase_to_stock_uom_factor": obj.purchase_to_stock_uom_factor, "purchase_to_invoice_uom_factor": obj.purchase_to_invoice_uom_factor, "purchase_price": obj.purchase_price, "purchase_account_id": obj.purchase_account_id.id, "purchase_tax_id": obj.purchase_tax_id.id, "supplier_id": obj.supplier_id.id, "sale_price": obj.sale_price, "sale_account_id": obj.sale_account_id.id, "sale_tax_id": obj.sale_tax_id.id, "sale_return_account_id": obj.sale_return_account_id.id, "sale_promotion_account_id": obj.sale_promotion_account_id.id, "sale_promotion_tax_id": obj.sale_promotion_tax_id.id, "cost_method": obj.cost_method, "stock_in_account_id": obj.stock_in_account_id.id, "stock_out_account_id": obj.stock_out_account_id.id, "bin_location": obj.bin_location, "sale_company_id": obj.sale_company_id.id, "attributes": [], } vals["attributes"] = [("delete_all", )] for attr in obj.attributes: vals["attributes"].append(("create", { "attribute_id": attr.attribute_id.id, "option_id": attr.option_id.id })) vals["pricelist_items"] = [("delete_all", )] for item in obj.pricelist_items: vals["pricelist_items"].append(("create", { "list_id": item.list_id.id, "price": item.price })) vals["images"] = [("delete_all", )] for image in obj.images: vals["images"].append(("create", { "image": image.image, "title": image.title })) print("vals", vals) if replace_id: self.write([replace_id], vals) new_id = replace_id else: new_id = self.create(vals) return { "next": { "name": "product", "mode": "form", "active_id": new_id, }, "flash": "New product copied from %s" % obj.name, } def get_landed_cost(self, ids, context={}): vals = {} for obj in self.browse(ids): amt = Decimal(obj.purchase_price or 0) * Decimal( 1 + (obj.purchase_duty_percent or 0) / 100) * Decimal(1 + (obj.purchase_ship_percent or 0) / 100) amt_cur = amt * Decimal(obj.purchase_currency_rate or 1) vals[obj.id] = { "landed_cost": amt, "landed_cost_conv": amt_cur, } return vals def get_auto_list_price(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.landed_cost_conv and obj.gross_profit and obj.gross_profit != 100: vals[obj.id] = obj.landed_cost_conv / (1 - obj.gross_profit / 100) else: vals[obj.id] = None return vals def get_max_discount(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.sale_price and obj.landed_cost_conv: vals[obj.id] = max(0, (obj.sale_price - obj.landed_cost_conv) / obj.sale_price * 100) else: vals[obj.id] = None return vals def get_price_index(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.sale_price and obj.landed_cost_conv: vals[obj.id] = obj.sale_price / obj.landed_cost_conv else: vals[obj.id] = None return vals def update_prices(self, context={}): print("update_prices") data = context["data"] purchase_price = data.get("purchase_price") purchase_currency_rate = data.get("purchase_currency_rate") purchase_duty_percent = data.get("purchase_duty_percent") purchase_ship_percent = data.get("purchase_ship_percent") if purchase_price: landed_cost = purchase_price * \ (1 + (purchase_duty_percent or 0) / 100) * (1 + (purchase_ship_percent or 0) / 100) landed_cost_conv = landed_cost * (purchase_currency_rate or 1) else: landed_cost = None landed_cost_conv = None gross_profit = data.get("gross_profit") if landed_cost_conv and gross_profit and gross_profit != 100: auto_list_price = landed_cost_conv / (1 - gross_profit / 100) else: auto_list_price = None sale_price = data.get("sale_price") if sale_price and landed_cost_conv: max_discount = max(0, (sale_price - landed_cost_conv) / sale_price * 100) price_index = sale_price / landed_cost else: max_discount = None price_index = None data.update({ "landed_cost": landed_cost, "landed_cost_conv": landed_cost_conv, "auto_list_price": auto_list_price, "max_discount": max_discount, "price_index": price_index, }) return data def get_ecom_sale_price(self, ids, context={}): raise Exception("Deprecated!") vals = {} for obj in self.browse(ids): if obj.ecom_discount_percent: p = (obj.sale_price or 0) * (1 - obj.ecom_discount_percent / 100) has_disc = True elif obj.ecom_special_price: p = obj.ecom_special_price has_disc = True else: p = obj.sale_price or 0 has_disc = False vals[obj.id] = { "ecom_sale_price": p, "ecom_has_discount": has_disc, } return vals def create_variants(self, ids, context={}): # deprecated print("##################################") print("product.create_variants", ids) obj = self.browse(ids[0]) if obj.type != "master": raise Exception("Not a master product") if not obj.custom_options: raise Exception("No custom options for this product") variants = [{}] for opt in obj.custom_options: new_variants = [] for variant in variants: for opt_val in opt.values: new_variant = variant.copy() new_variant[opt.code] = opt_val.code new_variants.append(new_variant) variants = new_variants print("variants", len(variants), variants) count = 1 for variant in variants: vals = { "code": "%s_VARIANT_%.2d" % (obj.code, count), "name": obj.name, "type": "stock", "uom_id": obj.uom_id.id, "parent_id": obj.id, "location_id": obj.location_id.id, "attributes": [], } for k, v in variant.items(): name = "_CUST_OPT_" + k res = get_model("product.attribute").search( [["name", "=", name]]) if res: attr_id = res[0] else: attr_id = get_model("product.attribute").create( {"name": name}) vals["attributes"].append(("create", { "attribute_id": attr_id, "value": v, })) get_model("product").create(vals) count += 1 return { "flash": "%d variants created" % len(variants), } def get_variant_attributes(self, ids, context={}): print("get_variant_attributes", ids) vals = {} for obj in self.browse(ids): attrs = [] attr_options = {} for variant in obj.variants: for attr in variant.attributes: if not attr.attribute_id or not attr.option_id: continue attr_code = attr.attribute_id.code attr_name = attr.attribute_id.name attrs.append((attr_name, attr_code)) opt_code = attr.option_id.code opt_name = attr.option_id.name opt_sequence = attr.option_id.sequence if attr_code == "color": opt_image = variant.image or attr.option_id.image else: opt_image = attr.option_id.image #attr_options.setdefault(attr_code,[]).append((opt_sequence,opt_name,opt_code,opt_image)) attr_options.setdefault(attr_code, []).append( (opt_sequence, opt_name, opt_code)) attrs = list(set(attrs)) res = [] for attr_name, attr_code in sorted(attrs): attr_vals = { "code": attr_code, "name": attr_name, "values": [], } attr_options[attr_code] = list(set(attr_options[attr_code])) #for opt_sequence,opt_name,opt_code,opt_image in sorted(attr_options[attr_code]): for opt_sequence, opt_name, opt_code in sorted( attr_options[attr_code]): attr_vals["values"].append({ "sequence": opt_sequence, "code": opt_code, "name": opt_name, #"image": opt_image, }) res.append(attr_vals) vals[obj.id] = res print("vals", vals) return vals def get_customer_price(self, ids, context={}): # XXX: make it faster pricelist_id = context.get("pricelist_id") pricelist_ids = context.get("pricelist_ids") if pricelist_ids is None and pricelist_id: pricelist_ids = [pricelist_id] vals = {} for obj in self.browse(ids): sale_price = None discount_text = None discount_percent = None if pricelist_ids: min_sale_price = None for item in obj.pricelist_items: if item.list_id.id in pricelist_ids: sale_price = (item.price or 0) if min_sale_price is None or sale_price < min_sale_price: min_sale_price = sale_price discount_text = item.discount_text discount_percent = item.discount_percent sale_price = min_sale_price if sale_price is None: sale_price = (obj.sale_price or 0) has_discount = sale_price < (obj.sale_price or 0) vals[obj.id] = { "customer_price": sale_price, "customer_has_discount": has_discount, "customer_discount_text": discount_text, "customer_discount_percent": discount_percent, } return vals def get_stock_qty(self, ids, context={}): vals = {} for obj in self.browse(ids): qty = 0 for loc in obj.locations: qty += loc.stock_qty vals[obj.id] = qty return vals def get_stock_lots(self, ids, context={}): db = database.get_connection() res = db.query( "SELECT b.product_id,b.location_id,p.sale_price,l.id AS lot_id,l.number,l.weight,SUM(b.qty_virt) AS qty FROM stock_balance b JOIN stock_lot l ON l.id=b.lot_id JOIN product p ON p.id=b.product_id WHERE b.product_id IN %s GROUP BY b.product_id,b.location_id,p.sale_price,l.id,l.number,l.weight ORDER BY l.expiry_date,l.received_date", tuple(ids)) lots = {} prod_lots = {} for r in res: k = (r.product_id, r.lot_id) if k not in lots: lot_vals = { "id": r.lot_id, "qty": 0, } lots[k] = lot_vals prod_lots.setdefault(r.product_id, []).append(lot_vals) lots[k]["qty"] += r.qty vals = {} for obj in self.browse(ids): vals[obj.id] = [ l["id"] for l in prod_lots.get(obj.id, []) if l["qty"] > 0 ] return vals def to_draft(self, ids, context={}): for obj in self.browse(ids): obj.write({"state": "draft", "is_published": False}) #XXX def approve(self, ids, context={}): for obj in self.browse(ids): res = self.search([["code", "=", obj.code], ["state", "=", "approved"], ["company_id", "=", obj.company_id.id]]) if res: repl_id = res[0] obj.copy(context={"replace_id": repl_id}) obj.write({"active": False}) else: vals = { "state": "approved", } if not obj.parent_id: group_ids = [] #XXX for group in obj.groups: group_ids.append(group.id) res = get_model("product.group").search( [["code", "=", "new"]]) if res and res[0] not in group_ids: group_ids.append(res[0]) vals.update({ "is_published": True, "groups": [("set", group_ids)] }) if not obj.approve_date: t = time.strftime("%Y-%m-%d %H:%M:%S") vals.update({"approve_date": t}) obj.write(vals) return { "flash": "Products approved", } def ecom_preview(self, ids, context={}): prod_id = ids[0] return { "next": { "type": "url", "url": "/ecom_product?product_id=%s" % prod_id, } } def get_sale_price_order_uom(self, ids, context={}): vals = {} for obj in self.browse(ids): factor = obj.sale_to_invoice_uom_factor or 1 vals[obj.id] = math.ceil((obj.sale_price or 0) * factor) return vals def create_thumbnails(self, ids, context={}): print("Product.create_thumbnails", ids) for obj in self.browse(ids): if not obj.image: continue dbname = database.get_active_db() if not dbname: return None fdir = os.path.join(os.getcwd(), "static", "db", dbname, "files") path = os.path.join(fdir, obj.image) basename, ext = os.path.splitext(obj.image) res = "," in basename if not res: rand = base64.urlsafe_b64encode(os.urandom(8)).decode() res = os.path.splitext(obj.image) basename, ext = res fname2 = basename + "," + rand + ext #rename image dest_path = fdir + "/" + fname2 print("destination path and file name ", dest_path) cmd = "cp %s %s" % (path, dest_path) os.system(cmd) obj.write({ 'image': fname2, }) utils.create_thumbnails(fname2) else: print("called", obj.image) utils.create_thumbnails(obj.image)
class EmailMessage(Model): _name = "email.message" _string = "Email Message" _name_field = "message_id" _fields = { "type": fields.Selection([["in", "Incoming"], ["out", "Outgoing"]], "Type"), # XXX: deprecated "date": fields.DateTime("Date", required=True, search=True, readonly=True), "from_addr": fields.Char("From", required=True, search=True, size=256), "to_addrs": fields.Char("To", size=256, search=True), "cc_addrs": fields.Char("Cc", size=256, search=True), "subject": fields.Char("Subject", required=True, size=128, search=True), # XXX: deprecated "content_type": fields.Selection([["plain", "Plain Text"], ["html", "HTML"]], "Content Type"), "body": fields.Text("Body", search=True), "state": fields.Selection([["draft", "Draft"], ["to_send", "To Send"], ["sent", "Sent"], ["delivered", "Delivered"], ["bounced", "Bounced"], ["rejected", "Rejected"], ["received", "Received"], ["error", "Error"]], "Status", required=True, search=True), "message_id": fields.Char("Message ID", size=256), "mailbox_message_uid": fields.Char("Mailbox Message UID"), "comments": fields.One2Many("message", "related_id", "Comments"), "name_id": fields.Reference([["contact", "Contact"], ["sale.lead", "Sales Lead"], ["mkt.target", "Marketing Target"]], "Contact Person", search=True), "related_id": fields.Reference( [["sale.opportunity", "Sales Opportunity"], ["sale.quot", "Quotation"], ["sale.order", "Sales Order"], ["job", "Service Order"], ["purchase.order", "Purchase Order"], ["account.invoice", "Invoice"], ["mkt.campaign", "Marketing Campaign"], ["ecom.cart", "Ecommerce Cart"]], "Related To"), "attachments": fields.One2Many("email.attach", "email_id", "Attachments"), "events": fields.One2Many("email.event", "email_id", "Email Events"), "opened": fields.Boolean("Opened", readonly=True, search=True), "clicked": fields.Boolean("Link Clicked", readonly=True, search=True), "in_reject_list": fields.Boolean("Black Listed", function="check_reject_list"), "parent_uid": fields.Char("Parent Unique ID", readonly=True, size=256), # XXX "parent_id": fields.Many2One("email.message", "Parent"), "mailbox_id": fields.Many2One("email.mailbox", "Mailbox", required=True, search=True), "open_detect": fields.Boolean("Open Detect", function="get_open_detect"), "num_attach": fields.Integer("# Attach.", function="get_num_attach"), "error_message": fields.Text("Error Message"), "template_id": fields.Many2One("email.template", "Email Template"), } _order = "date desc" def _get_mailbox(self, context={}): user_id = get_active_user() res = get_model("email.mailbox").search([["type", "=", "out"]], order="id") if not res: return None return res[0] def _get_from_addr(self, context={}): user_id = get_active_user() user = get_model("base.user").browse(user_id) return user.email def _get_to_addrs(self, context={}): defaults = context.get("defaults") if not defaults: return None parent_id = int(defaults.get("parent_id")) if not parent_id: return None parent = get_model("email.message").browse(parent_id) return parent.from_addr def _get_subject(self, context={}): defaults = context.get("defaults") if not defaults: return None parent_id = int(defaults.get("parent_id")) if not parent_id: return None parent = get_model("email.message").browse(parent_id) s = parent.subject return "Re: " + s def _get_body(self, context={}): defaults = context.get("defaults") if not defaults: return None parent_id = int(defaults.get("parent_id")) if not parent_id: return None parent = get_model("email.message").browse(parent_id) d = datetime.strptime(parent.date, "%Y-%m-%d %H:%M:%S") pbody = parent.body m = re.search("<body>(.*)</body>", pbody, re.DOTALL) if m: pbody = m.group(1) m = re.search("<BODY>(.*)</BODY>", pbody, re.DOTALL) if m: pbody = m.group(1) body = "<div>On %s, %s wrote:" % (d.strftime("%a, %b %d, %Y at %H:%M"), parent.from_addr) body += '<blockquote style="margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex">' + \ pbody + "</blockquote>" body += "</div>" return body _defaults = { "type": "out", "state": "draft", "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "mailbox_id": _get_mailbox, "from_addr": _get_from_addr, "to_addrs": _get_to_addrs, "subject": _get_subject, "body": _get_body, } def to_send(self, ids, context={}): for obj in self.browse(ids): obj.write({"state": "to_send"}) def to_draft(self, ids, context={}): for obj in self.browse(ids): obj.write({"state": "draft"}) def send_emails(self, context={}): # FIXME print("send_emails") mailbox_ids = get_model("email.mailbox").search( [["account_id.type", "in", ["smtp", "mailgun"]]]) get_model("email.mailbox").send_emails(mailbox_ids) def check_sent_emails(self, context={}): print("send_sent_emails") res = get_model("email.account").search([["type", "=", "mailgun"]]) # XXX if not res: return acc_id = res[0] get_model("email.account").check_sent_emails([acc_id]) def send_from_template(self, template=None, from_addr=None, to_addrs=None, context={}): if not template: raise Exception("Missing template") res = get_model("email.template").search([["name", "=", template]]) if not res: raise Exception("Template not found: %s" % template) tmpl_id = res[0] tmpl = get_model("email.template").browse(tmpl_id) trigger_model = context.get("trigger_model") if not trigger_model: raise Exception("Missing trigger model") tm = get_model(trigger_model) trigger_ids = context.get("trigger_ids") if trigger_ids is None: raise Exception("Missing trigger ids") user_id = get_active_user() if user_id: user = get_model("base.user").browse(user_id) else: user = None for obj in tm.browse(trigger_ids): tmpl_ctx = {"obj": obj, "user": user, "context": context} get_model("email.template").create_email([tmpl_id], tmpl_ctx) def import_email(self, vals): print("import_email from=%s to=%s cc=%s subject=%s" % (vals["from_addr"], vals.get("to_addrs"), vals.get("cc_addrs"), vals["subject"])) def _check_addr(addrs): if not addrs: return False addr_list = addrs.split(",") for addr in addr_list: addr = addr.strip() res = get_model("base.user").search([["email", "=ilike", addr]]) if res: return True res = get_model("contact").search([["email", "=ilike", addr]]) if res: return True res = get_model("sale.lead").search([["email", "=ilike", addr]]) if res: return True return False if not _check_addr(vals["from_addr"]): print_color( "email ignored: unknown 'from' address: %s" % vals["from_addr"], "red") return None if not _check_addr(vals.get("to_addrs")) and not _check_addr( vals.get("cc_addrs")): print_color( "email ignored: unknown 'to' and 'cc' address: to=%s cc=%s" % (vals.get("to_addrs"), vals.get("cc_addrs")), "red") return None new_id = self.create(vals) print_color("email imported: id=%d" % new_id, "green") self.link_emails([new_id]) self.trigger([new_id], "received") return new_id def link_emails(self, ids, context={}): def _get_addr_contact(addr): res = get_model("contact").search([["email", "=ilike", addr]]) if res: return res[0] return None def _get_msg_name(obj): if obj.from_addr: contact_id = _get_addr_contact(obj.from_addr) if contact_id: return "contact,%d" % contact_id if obj.to_addrs: for addr in obj.to_addrs.split(","): addr = addr.strip() contact_id = _get_addr_contact(addr) if contact_id: return "contact,%d" % contact_id if obj.cc_addrs: for addr in obj.cc_addrs.split(","): addr = addr.strip() contact_id = _get_addr_contact(addr) if contact_id: return "contact,%d" % contact_id def _get_msg_related(obj): m = re.search("\[(.*?)\]", obj.subject or "") if not m: return None num = m.group(1).strip() res = get_model("sale.opportunity").search( [["name", "=ilike", num]]) if res: return "sale.opportunity,%s" % res[0] res = get_model("sale.quot").search([["number", "=ilike", num]]) if res: return "sale.quot,%s" % res[0] res = get_model("sale.order").search([["number", "=ilike", num]]) if res: return "sale.order,%s" % res[0] res = get_model("job").search([["number", "=ilike", num]]) if res: return "job,%s" % res[0] res = get_model("account.invoice").search( [["number", "=ilike", num]]) if res: return "account.invoice,%s" % res[0] return None for obj in self.browse(ids): if not obj.name_id: name_id = _get_msg_name(obj) if name_id: obj.write({"name_id": name_id}) if not obj.related_id: related_id = _get_msg_related(obj) if related_id: obj.write({"related_id": related_id}) def copy_to_lead(self, context={}): trigger_ids = context[ "trigger_ids"] # XXX: put in workflow action args instead (need eval)? obj = self.browse(trigger_ids)[0] vals = { "last_name": "/", "email": obj.from_addr, "description": obj.body, } lead_id = get_model("sale.lead").create(vals) obj.write({"related_id": "sale.lead,%d" % lead_id}) def check_reject_list(self, ids, context={}): vals = {} for obj in self.browse(ids): rejected = False for addr in obj.to_addrs.split(","): addr = addr.strip() res = get_model("email.reject").search([["email", "=", addr]]) if res: rejected = True break vals[obj.id] = rejected return vals def create_from_string(self, email_str, mailbox_id=None, mailbox_message_uid=None): print("email.message create_from_string (%d bytes)" % len(email_str), mailbox_message_uid) msg = email.message_from_bytes(email_str) msg_id = msg.get("message-id") print("msg_id", msg_id) res = get_model("email.message").search( [["mailbox_id", "=", mailbox_id], ["message_id", "=", msg_id]]) if res: return res[0] def dec_header(data): dh = decode_header(data or "") s = "" for data, charset in dh: if isinstance(data, str): s += data else: try: s += data.decode(conv_charset(charset) or "utf-8") except: s += data.decode("utf-8", errors="replace") return s def dec_date(data): print("dec_date", data) res = parsedate_tz(data or "") if not res: return time.strftime("%Y-%m-%d %H:%M:%S") d = datetime.fromtimestamp(email.utils.mktime_tz(res)) return d.strftime("%Y-%m-%d %H:%M:%S") body_text = [] body_html = [] attachments = [] content_ids = {} for i, part in enumerate(msg.walk()): print("-" * 80) print("part #%d" % i) c_type = part.get_content_type() print("c_type", c_type) c_disp = part.get("Content-Disposition") print("c_disp", c_disp) c_id = part.get("Content-ID") print("c_id", c_id) if c_disp: fname = part.get_filename() data = part.get_payload(decode=True) attachments.append((fname, data)) elif c_id: data = part.get_payload(decode=True) rand = base64.urlsafe_b64encode(os.urandom(8)).decode() fname = "email-inline-content," + rand if c_type == "image/png": ext = ".png" elif c_type == "image/jpeg": ext = ".jpg" elif c_type == "image/gif": ext = ".gif" else: ext = None if ext: fname += "." + ext content_ids[c_id] = (fname, data) else: if c_type == "text/plain": v = part.get_payload(decode=True).decode(errors="replace") body_text.append(v) elif c_type == "text/html": v = part.get_payload(decode=True).decode(errors="replace") body_html.append(v) else: continue if body_html: body = body_html[0] elif body_text: body = "<html><body>" for t in body_text: body += "<pre>" + t + "</pre>" body += "</body></html>" else: body = "" def _replace(m): cid = m.group(1) cid = "<" + cid + ">" if cid not in content_ids: return m.group(0) fname = content_ids[cid][0] dbname = get_active_db() return "/static/db/" + dbname + "/files/" + fname body = re.sub('"cid:(.*?)"', _replace, body) email_vals = { "type": "in", "state": "received", "date": dec_date(msg["date"]), "from_addr": parseaddr(msg["from"])[1][:64], "subject": dec_header(msg["subject"])[:128], "body": body, "message_id": msg_id, "parent_uid": msg.get("in-reply-to"), "mailbox_id": mailbox_id, "mailbox_message_uid": mailbox_message_uid, } if msg.get_all("to"): email_vals["to_addrs"] = ",".join( [parseaddr(x)[1] for x in msg.get_all("to")])[:256] if msg.get_all("cc"): email_vals["cc_addrs"] = ",".join( [parseaddr(x)[1] for x in msg.get_all("cc")])[:256] email_id = get_model("email.message").create(email_vals) for fname, data in attachments: if not fname: continue rand = base64.urlsafe_b64encode(os.urandom(8)).decode() res = os.path.splitext(fname) fname2 = res[0] + "," + rand + res[1] dbname = get_active_db() fdir = os.path.join("static", "db", dbname, "files") if not os.path.exists(fdir): os.makedirs(fdir) path = os.path.join(fdir, fname2) open(path, "wb").write(data) vals = { "email_id": email_id, "file": fname2, } get_model("email.attach").create(vals) for fname, data in content_ids.values(): dbname = get_active_db() fdir = os.path.join("static", "db", dbname, "files") if not os.path.exists(fdir): os.makedirs(fdir) path = os.path.join(fdir, fname) open(path, "wb").write(data) def reply(self, ids, context={}): obj = self.browse(ids)[0] return { "next": { "name": "email", "mode": "form", "defaults": { "parent_id": obj.id, }, } } def get_open_detect(self, ids, context={}): vals = {} for obj in self.browse(ids): if obj.mailbox_id and obj.mailbox_id.type == "in": obj.write({"opened": True}) vals[obj.id] = None return vals def send(self, ids, context={}): print("EmailMessage.send", ids) for obj in self.browse(ids): if obj.state not in ("draft", "to_send"): continue mailbox = obj.mailbox_id if not mailbox: raise Exception("Missing mailbox in email %s" % obj.id) account = mailbox.account_id if not account: raise Exception("Missing account in mailbox %s of email %s" % (mailbox.name, obj.id)) if obj.in_reject_list: print("WARNING: email in reject list: %s" % obj.id) obj.write({"state": "rejected"}) continue if account.type == "smtp": obj.send_email_smtp() elif account.type == "mailgun": obj.send_email_mailgun() else: raise Exception("Invalid email account type") return { "next": { "name": "email", "tab_no": 1, }, } def send_email_smtp(self, ids, context={}): print("send_email_smtp", ids) obj = self.browse(ids)[0] if obj.state not in ("draft", "to_send"): return try: mailbox = obj.mailbox_id if not mailbox: raise Exception("Missing mailbox") account = mailbox.account_id if account.type != "smtp": raise Exception("Invalid email account type") if account.security == "SSL": server = smtplib.SMTP_SSL(account.host, account.port or 465, timeout=30) else: server = smtplib.SMTP(account.host, account.port or 587, timeout=30) server.ehlo() if account.security == "starttls": server.starttls() if account.user: server.login(account.user, account.password) msg = MIMEMultipart() msg.set_charset("utf-8") msg["From"] = obj.from_addr msg["To"] = obj.to_addrs if obj.cc_addrs: msg["Cc"] = obj.cc_addrs msg["Subject"] = Header(obj.subject, "utf-8") msg.attach(MIMEText(obj.body, "html", "utf-8")) for attach in obj.attachments: path = utils.get_file_path(attach.file) data = open(path, "rb").read() part = MIMEBase('application', "octet-stream") part.set_payload(data) encode_base64(part) part.add_header('Content-Disposition', 'attachment; filename="%s"' % attach.file) msg.attach(part) to_addrs = obj.to_addrs.split(",") cc_addrs = obj.cc_addrs.split(",") if obj.cc_addrs else [] server.sendmail(obj.from_addr, to_addrs + cc_addrs, msg.as_string()) obj.write({"state": "sent"}) server.quit() except Exception as e: print("WARNING: failed to send email %s" % obj.id) import traceback traceback.print_exc() obj.write({"state": "error", "error_message": str(e)}) def send_email_mailgun(self, ids, context={}): print("send_emails_mailgun", ids) obj = self.browse(ids)[0] if obj.state not in ("draft", "to_send"): return try: mailbox = obj.mailbox_id if not mailbox: raise Exception("Missing mailbox") account = mailbox.account_id if account.type != "mailgun": raise Exception("Invalid email account type") url = "https://api.mailgun.net/v2/%s/messages" % account.user to_addrs = [] for a in obj.to_addrs.split(","): a = a.strip() if not utils.check_email_syntax(a): raise Exception("Invalid email syntax: %s" % a) to_addrs.append(a) if not to_addrs: raise Exception("Missing recipient address") data = { "from": obj.from_addr, "to": to_addrs, "subject": obj.subject, } if obj.cc_addrs: data["cc"] = [a.strip() for a in obj.cc_addrs.split(",")] data["html"] = obj.body or "<html><body></body></html>" files = [] for attach in obj.attachments: path = utils.get_file_path(attach.file) f = open(path, "rb") files.append(("attachment", f)) r = requests.post(url, auth=("api", account.password), data=data, files=files, timeout=15) try: res = json.loads(r.text) msg_id = res["id"] except: raise Exception("Invalid mailgun response: %s" % r.text) obj.write({"state": "sent", "message_id": msg_id}) except Exception as e: print("WARNING: failed to send email %s" % obj.id) import traceback traceback.print_exc() obj.write({"state": "error", "error_message": str(e)}) def get_emails(self, context={}): print("EmailMessage.get_emails") for mailbox in get_model("email.mailbox").search_browse([]): acc = mailbox.account_id if not acc: continue if acc.type == "imap": mailbox.fetch_emails_imap() mailbox.get_flags_imap() elif acc.type == "pop": mailbox.fetch_emails_pop() elif acc.type == "mailgun": mailbox.get_email_events_mailgun() return { "next": { "name": "email", "tab_no": 0, }, } def mark_opened(self, ids, context={}): self.write(ids, {"opened": True}) def get_num_attach(self, ids, context={}): vals = {} for obj in self.browse(ids): vals[obj.id] = len(obj.attachments) # XXX: speed return vals
class Cart(Model): _name = "ecom2.cart" _string = "Cart" _name_field = "number" _audit_log = True _fields = { "number": fields.Char("Number", required=True, search=True), "date": fields.DateTime("Date Created", required=True, search=True), "customer_id": fields.Many2One("contact", "Customer", search=True), "lines": fields.One2Many("ecom2.cart.line", "cart_id", "Lines"), "ship_amount_details": fields.Json("Shipping Amount Details", function="get_ship_amount_details"), "amount_ship": fields.Decimal("Shipping Amount", function="get_amount_ship"), "amount_total": fields.Decimal("Total Amount", function="get_total"), "sale_orders": fields.One2Many("sale.order", "related_id", "Sales Orders"), "delivery_date": fields.Date("Delivery Date"), "ship_address_id": fields.Many2One("address", "Shipping Address"), "bill_address_id": fields.Many2One("address", "Billing Address"), "delivery_slot_id": fields.Many2One("delivery.slot", "Peferred Delivery Slot"), "ship_method_id": fields.Many2One("ship.method", "Shipping Method"), "pay_method_id": fields.Many2One("payment.method", "Payment Method"), "logs": fields.One2Many("log", "related_id", "Audit Log"), "state": fields.Selection([["draft", "Draft"], ["confirmed", "Confirmed"], ["canceled", "Canceled"]], "Status", required=True), "payment_methods": fields.Json("Payment Methods", function="get_payment_methods"), "delivery_delay": fields.Integer("Delivery Delay (Days)", function="get_delivery_delay"), "delivery_slots": fields.Json("Delivery Slots", function="get_delivery_slots"), "delivery_slots_str": fields.Text("Delivery Slots", function="get_delivery_slots_str"), "date_delivery_slots": fields.Json("Date Delivery Slots", function="get_date_delivery_slots"), "comments": fields.Text("Comments"), "transaction_no": fields.Char("Payment Transaction No.", search=True), "currency_id": fields.Many2One("currency", "Currency", required=True), "invoices": fields.One2Many("account.invoice", "related_id", "Invoices"), "company_id": fields.Many2One("company", "Company"), "voucher_id": fields.Many2One("sale.voucher", "Voucher"), "ship_addresses": fields.Json("Shipping Addresses", function="get_ship_addresses"), "amount_voucher": fields.Decimal("Voucher Amount", function="get_amount_voucher", function_multi=True), "voucher_error_message": fields.Text("Voucher Error Message", function="get_amount_voucher", function_multi=True), "free_ship_address": fields.Boolean("Is free Ship"), } _order = "date desc" def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence(type="ecom_cart") 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 = access.get_active_user() access.set_active_user(1) res = self.search([["number", "=", num]]) access.set_active_user(user_id) if not res: return num get_model("sequence").increment_number(seq_id, context=context) def _get_currency(self, context={}): res = get_model("company").search([]) # XXX if not res: return company_id = res[0] access.set_active_company(company_id) settings = get_model("settings").browse(1) return settings.currency_id.id def _get_company(self, context={}): res = get_model("company").search([]) # XXX if res: return res[0] def _get_ship_method(self, context={}): res = get_model("ship.method").search([], order="sequence") if res: return res[0] _defaults = { "date": lambda *a: time.strftime("%Y-%m-%d %H:%M:%S"), "number": _get_number, "state": "draft", "currency_id": _get_currency, "company_id": _get_company, "ship_method_d": _get_ship_method, "free_ship_address": False, } def get_ship_amount_details(self, ids, context={}): print("get_ship_amount_details", ids) vals = {} for obj in self.browse(ids): delivs = [] for line in obj.lines: date = line.delivery_date meth_id = line.ship_method_id.id or line.cart_id.ship_method_id.id addr_id = line.ship_address_id.id or line.cart_id.ship_address_id.id if not date or not meth_id or not addr_id: continue delivs.append((date, meth_id, addr_id)) delivs = list(set(delivs)) details = [] for date, meth_id, addr_id in delivs: ctx = { "contact_id": obj.customer_id.id, "ship_address_id": addr_id, } meth = get_model("ship.method").browse(meth_id, context=ctx) details.append({ "ship_addr_id": addr_id, "date": date, "ship_method_id": meth.id, "ship_amount": meth.ship_amount, }) vals[obj.id] = details return vals def get_amount_ship(self, ids, context={}): print("get_amount_ship", ids) vals = {} for obj in self.browse(ids): ship_amt = 0 for d in obj.ship_amount_details: ship_amt += d["ship_amount"] or 0 vals[obj.id] = ship_amt return vals def get_total(self, ids, context={}): vals = {} for obj in self.browse(ids): amt = 0 for line in obj.lines: amt += line.amount vals[obj.id] = amt + obj.amount_ship - obj.amount_voucher return vals def get_payment_methods(self, ids, context={}): res = [] for obj in get_model("payment.method").search_browse([]): res.append({ "id": obj.id, "name": obj.name, }) return {ids[0]: res} def confirm(self, ids, context={}): obj = self.browse(ids[0]) user_id = context.get("user_id") # XX: remove this if user_id: user_id = int(user_id) user = get_model("base.user").browse(user_id) if user.contact_id: obj.write({"customer_id": user.contact_id.id}) access.set_active_company(1) # XXX vals = { "contact_id": obj.customer_id.id, "ship_address_id": obj.ship_address_id.id, "ship_method_id": obj.ship_method_id.id, "bill_address_id": obj.bill_address_id.id, "due_date": obj.delivery_date, "lines": [], "related_id": "ecom2.cart,%s" % obj.id, "delivery_slot_id": obj.delivery_slot_id.id, "pay_method_id": obj.pay_method_id.id, "other_info": obj.comments, "voucher_id": obj.voucher_id.id, "ref": obj.comments, # XXX } print(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>Ship Method Id ", ship_method_id) for line in obj.lines: prod = line.product_id if line.lot_id and line.qty_avail <= 0: raise Exception("Lot is out of stock (%s)" % prod.name) if not prod.locations: raise Exception("Can't find location for product %s" % prod.code) location_id = prod.locations[0].location_id.id line_vals = { "product_id": prod.id, "description": prod.description, "qty": line.qty, "uom_id": line.uom_id.id, "unit_price": line.unit_price, "location_id": location_id, "lot_id": line.lot_id.id, "due_date": line.delivery_date, "delivery_slot_id": obj.delivery_slot_id.id, "ship_address_id": line.ship_address_id.id, } vals["lines"].append(("create", line_vals)) for ship in obj.ship_amount_details: meth_id = ship["ship_method_id"] amount = ship["ship_amount"] meth = get_model("ship.method").browse(meth_id) prod = meth.product_id if not prod: raise Exception("Missing product in shipping method %s" % meth.name) line_vals = { "product_id": prod.id, "description": prod.description, "qty": 1, "uom_id": prod.uom_id.id, "unit_price": amount, } vals["lines"].append(("create", line_vals)) sale_id = get_model("sale.order").create(vals) sale = get_model("sale.order").browse(sale_id) sale.confirm() obj.write({"state": "confirmed"}) obj.trigger("confirm_order") return { "sale_id": sale_id, } def cancel_order(self, ids, context={}): obj = self.browse(ids[0]) for sale in obj.sale_orders: if sale.state == "voided": continue for inv in sale.invoices: if inv.state != "voided": raise Exception( "Can not cancel order %s because there are linked invoices" % sale.number) for pick in sale.pickings: if pick.state == "voided": continue pick.void() sale.void() for inv in obj.invoices: if inv.state != "voided": raise Exception( "Can not cancel cart %s because there are linked invoices" % obj.number) obj.write({"state": "canceled"}) def payment_received(self, ids, context={}): obj = self.browse(ids[0]) if obj.state != "waiting_payment": raise Exception("Cart is not waiting payment") res = obj.sale_orders.copy_to_invoice() inv_id = res["invoice_id"] inv = get_model("account.invoice").browse(inv_id) inv.write({"related_id": "ecom2.cart,%s" % obj.id}) inv.post() method = obj.pay_method_id if not method: raise Exception("Missing payment method in cart %s" % obj.number) if not method.account_id: raise Exception("Missing account in payment method %s" % method.name) transaction_no = context.get("transaction_no") pmt_vals = { "type": "in", "pay_type": "invoice", "contact_id": inv.contact_id.id, "account_id": method.account_id.id, "lines": [], "company_id": inv.company_id.id, "transaction_no": transaction_no, } line_vals = { "invoice_id": inv_id, "amount": inv.amount_total, } pmt_vals["lines"].append(("create", line_vals)) pmt_id = get_model("account.payment").create(pmt_vals, context={"type": "in"}) get_model("account.payment").post([pmt_id]) obj.write({"state": "paid"}) for sale in obj.sale_orders: for pick in sale.pickings: if pick.state == "pending": pick.approve() def to_draft(self, ids, context={}): obj = self.browse(ids[0]) obj.write({"state": "draft"}) def set_qty(self, ids, prod_id, qty, context={}): print("Cart.set_qty", ids, prod_id, qty) obj = self.browse(ids[0]) line_id = None for line in obj.lines: if line.product_id.id == prod_id and not line.lot_id: line_id = line.id break if line_id: if qty == 0: get_model("ecom2.cart.line").delete([line_id]) else: get_model("ecom2.cart.line").write([line_id], {"qty": qty}) else: if qty != 0: get_model("ecom2.cart.line").create({ "cart_id": obj.id, "product_id": prod_id, "qty": qty }) def set_date_qty(self, ids, due_date, prod_id, qty, context={}): print("Cart.set_date_qty", ids, due_date, prod_id, qty) obj = self.browse(ids[0]) line_id = None for line in obj.lines: if line.delivery_date == due_date and line.product_id.id == prod_id and not line.lot_id: line_id = line.id break if line_id: if qty == 0: get_model("ecom2.cart.line").delete([line_id]) else: get_model("ecom2.cart.line").write([line_id], {"qty": qty}) else: if qty != 0: ctx = { "cart_id": obj.id, "delivery_date": due_date, "product_id": prod_id } get_model("ecom2.cart.line").create( { "cart_id": obj.id, "product_id": prod_id, "qty": qty, "delivery_date": due_date }, context=ctx) def add_lot(self, ids, prod_id, lot_id, context={}): print("Cart.add_lot", ids, prod_id, lot_id) obj = self.browse(ids[0]) line_id = None for line in obj.lines: if line.product_id.id == prod_id and line.lot_id.id == lot_id: line_id = line.id break if line_id: raise Exception("Lot already added to cart") get_model("ecom2.cart.line").create({ "cart_id": obj.id, "product_id": prod_id, "lot_id": lot_id, "qty": 1 }) def remove_lot(self, ids, prod_id, lot_id, context={}): obj = self.browse(ids[0]) line_id = None for line in obj.lines: if line.product_id.id == prod_id and line.lot_id.id == lot_id: line_id = line.id break if not line_id: raise Exception("Lot not found in cart") get_model("ecom2.cart.line").delete([line_id]) def set_qty_auto_select_lot(self, ids, prod_id, qty, context={}): print("Cart.set_qty_auto_select_lot", ids, prod_id, qty) obj = self.browse(ids[0]) cur_qty = 0 for line in obj.lines: if line.product_id.id == prod_id: cur_qty += line.qty diff_qty = qty - cur_qty if diff_qty > 0: self.add_lots_auto_select(ids, prod_id, diff_qty, context=context) elif diff_qty < 0: self.remove_lots_auto_select(ids, prod_id, -diff_qty, context=context) def add_lots_auto_select(self, ids, prod_id, add_qty, context={}): print("Cart.add_lots_auto_select", ids, prod_id, add_qty) obj = self.browse(ids[0]) exclude_lot_ids = [] for line in obj.lines: if line.product_id.id == prod_id and line.lot_id: exclude_lot_ids.append(line.lot_id.id) prod = get_model("product").browse(prod_id) avail_qty = prod.stock_qty add_lot_ids = [] for lot in prod.stock_lots: if len(add_lot_ids) >= avail_qty: break if lot.id not in exclude_lot_ids: add_lot_ids.append(lot.id) if len(add_lot_ids) >= add_qty: break print("add_lot_ids", add_lot_ids) for lot_id in add_lot_ids: get_model("ecom2.cart.line").create({ "cart_id": obj.id, "product_id": prod_id, "lot_id": lot_id, "qty": 1 }) remain_qty = add_qty - len(add_lot_ids) print("remain_qty", remain_qty) if remain_qty > 0: found_line = None for line in obj.lines: if line.product_id.id == prod_id and not line.lot_id: found_line = line break if found_line: found_line.write({"qty": found_line.qty + remain_qty}) else: get_model("ecom2.cart.line").create({ "cart_id": obj.id, "product_id": prod_id, "qty": remain_qty }) def remove_lots_auto_select(self, ids, prod_id, remove_qty, context={}): print("Cart.remove_lots_auto_select", ids, prod_id, remove_qty) obj = self.browse(ids[0]) remain_qty = remove_qty for line in obj.lines: if line.product_id.id == prod_id and not line.lot_id: if line.qty <= remain_qty: remain_qty -= line.qty line.delete() else: line.write({"qty": line.qty - remain_qty}) remain_qty = 0 print("remain_qty", remain_qty) if remain_qty > 0: lots = [] for line in obj.lines: if line.product_id.id == prod_id and line.lot_id: d = line.lot_id.received_date or "1900-01-01" lots.append((d, line.id)) lots.sort() del_ids = [] for d, line_id in lots: del_ids.append(line_id) if len(del_ids) >= remain_qty: break print("del_ids", del_ids) get_model("ecom2.cart.line").delete(del_ids) def get_delivery_delay(self, ids, context={}): vals = {} for obj in self.browse(ids): vals[obj.id] = max(l.delivery_delay for l in obj.lines) if obj.lines else 0 return vals def get_delivery_slots(self, ids, context={}): print("get_delivery_slots", ids) obj = self.browse(ids[0]) settings = get_model("ecom2.settings").browse(1) max_days = settings.delivery_max_days if not max_days: return {obj.id: []} min_hours = settings.delivery_min_hours or 0 d_from = date.today() + timedelta(days=obj.delivery_delay) d_to = d_from + timedelta(days=max_days) d = d_from slots = [] for slot in get_model("delivery.slot").search_browse([]): slots.append([slot.id, slot.name, slot.time_from]) slot_num_sales = {} for sale in get_model("sale.order").search_browse( [["plr_order_type", "=", "grocery"], ["date", ">=", time.strftime("%Y-%m-%d")]]): k = (sale.due_date, sale.delivery_slot_id.id) slot_num_sales.setdefault(k, 0) slot_num_sales[k] += 1 print("slot_num_sales", slot_num_sales) slot_caps = {} for cap in get_model("delivery.slot.capacity").search_browse([]): k = (cap.slot_id.id, int(cap.weekday)) slot_caps[k] = cap.capacity print("slot_caps", slot_caps) delivery_weekdays = None for line in obj.lines: prod = line.product_id if prod.delivery_weekdays: days = [int(w) for w in prod.delivery_weekdays.split(",")] if delivery_weekdays == None: delivery_weekdays = days else: delivery_weekdays = [ d for d in delivery_weekdays if d in days ] days = [] now = datetime.now() tomorrow = now + timedelta(days=1) tomorrow_seconds = 0 if settings.work_time_start and settings.work_time_end: today_end = datetime.strptime( date.today().strftime("%Y-%m-%d") + " " + settings.work_time_end, "%Y-%m-%d %H:%M") tomorrow_start = datetime.strptime( tomorrow.strftime("%Y-%m-%d") + " " + settings.work_time_start, "%Y-%m-%d %H:%M") if now < today_end and now.weekday() != 6: remain_seconds = (today_end - now).total_seconds() else: remain_seconds = 0 if remain_seconds < min_hours * 3600: tomorrow_seconds = min_hours * 3600 - remain_seconds print("tomorrow_seconds=%s" % tomorrow_seconds) while d <= d_to: ds = d.strftime("%Y-%m-%d") res = get_model("hr.holiday").search([["date", "=", ds]]) if res: d += timedelta(days=1) continue w = d.weekday() if w == 6 or delivery_weekdays is not None and w not in delivery_weekdays: d += timedelta(days=1) continue day_slots = [] for slot_id, slot_name, from_time in slots: t_from = datetime.strptime(ds + " " + from_time + ":00", "%Y-%m-%d %H:%M:%S") capacity = slot_caps.get((slot_id, w)) num_sales = slot_num_sales.get((ds, slot_id), 0) state = "avail" if d == now.date(): if t_from < now or (t_from - now).total_seconds( ) < min_hours * 3600 or tomorrow_seconds: state = "full" elif d == tomorrow.date() and tomorrow_seconds: if (t_from - tomorrow_start).total_seconds() < tomorrow_seconds: state = "full" if capacity is not None and num_sales >= capacity: state = "full" day_slots.append( [slot_id, slot_name, state, num_sales, capacity]) days.append([ds, day_slots]) d += timedelta(days=1) print("days:") pprint(days) return {obj.id: days} def get_delivery_slots_str(self, ids, context={}): vals = {} for obj in self.browse(ids): s = "" for d, slots in obj.delivery_slots: s += "- Date: %s\n" % d for slot_id, name, state, num_sales, capacity in slots: s += " - %s: %s (%s/%s)\n" % (name, state, num_sales, capacity or "-") vals[obj.id] = s return vals def get_date_delivery_slots(self, ids, context={}): print("get_date_delivery_slots", ids) obj = self.browse(ids[0]) slots = [] for slot in get_model("delivery.slot").search_browse([]): slots.append([slot.id, slot.name]) dates = [] for line in obj.lines: d = line.delivery_date if d: dates.append(d) dates = list(set(dates)) date_slots = {} for d in dates: date_slots[d] = slots # TODO: use capacity? return {obj.id: date_slots} def get_ship_addresses(self, ids, context={}): obj = self.browse(ids[0]) settings = get_model("ecom2.settings").browse(1) contact = obj.customer_id addrs = [] if contact: for a in contact.addresses: addr_vals = { "id": a.id, "name": a.address, } if obj.ship_method_id: # TODO: handle general case for different shipping methods per order meth_id = obj.ship_method_id.id ctx = {"ship_address_id": a.id} meth = get_model("ship.method").browse(meth_id, context=ctx) addr_vals["ship_amount"] = meth.ship_amount else: addr_vals["ship_amount"] = 0 addrs.append(addr_vals) for a in settings.extra_ship_addresses: addr_vals = { "id": a.id, "name": a.address or "", } if obj.ship_method_id: meth_id = obj.ship_method_id.id ctx = {"ship_address_id": a.id} meth = get_model("ship.method").browse(meth_id, context=ctx) addr_vals["ship_amount"] = meth.ship_amount else: addr_vals["ship_amount"] = 0 addrs.append(addr_vals) return {obj.id: addrs} def apply_voucher_code(self, ids, voucher_code, context={}): obj = self.browse(ids[0]) res = get_model("sale.voucher").search([["code", "=", voucher_code]]) if not res: raise Exception("Invalid voucher code") voucher_id = res[0] obj.write({"voucher_id": voucher_id}) def clear_voucher(self, ids, context={}): obj = self.browse(ids[0]) obj.write({"voucher_id": None}) def get_amount_voucher(self, ids, context={}): print("get_amount_voucher", ids) vals = {} for obj in self.browse(ids): voucher = obj.voucher_id if voucher: ctx = { "contact_id": obj.customer_id.id, "amount_total": 0, "products": [], } for line in obj.lines: ctx["amount_total"] += line.amount ctx["products"].append({ "product_id": line.product_id.id, "unit_price": line.unit_price, "qty": line.qty, "uom_id": line.uom_id.id, "amount": line.amount, }) ctx["amount_total"] += obj.amount_ship res = voucher.apply_voucher(context=ctx) disc_amount = res.get("discount_amount", 0) error_message = res.get("error_message") else: disc_amount = 0 error_message = None vals[obj.id] = { "amount_voucher": disc_amount, "voucher_error_message": error_message, } return vals def update_date_delivery(self, ids, date, vals, context={}): print("cart.update_date_delivery", ids, date, vals) obj = self.browse(ids[0]) settings = get_model("ecom2.settings").browse(1) for line in obj.lines: if line.delivery_date == date: line.write(vals) #for a in settings.extra_ship_addresses: #if a.id == vals['ship_address_id']: #return {'free_ship':True} return {'free_ship': False} def empty_cart(self, ids, context={}): obj = self.browse(ids[0]) obj.write({"lines": [("delete_all", )]}) def check_due_dates(self, ids, context={}): obj = self.browse(ids[0]) today = time.strftime("%Y-%m-%d") if obj.delivery_date and obj.delivery_date < today: raise Exception("Delivery date is in the past %s" % obj.delivery_date) for line in obj.lines: if line.delivery_date and line.delivery_date < today: raise Exception( "Delivery date is in the past %s for product %s" % (line.delivery_date, line.product_id.name)) def free_ship_address(self, ids, context={}): obj = self.browse(ids[0]) settings = get_model("ecom2.settings").browse(1) data = [] for a in settings.extra_ship_addresses: if obj.ship_method_id: data["is_free"] = False else: data["is_free"] = True data.append(data) return {obj.id: addrs}
class StockLot(Model): _name = "stock.lot" _string = "Lot / Serial Number" _name_field = "number" _key = ["number"] _fields = { "number": fields.Char("Number", required=True, search=True), "received_date": fields.DateTime("Received Date", search=True), "expiry_date": fields.Date("Expiry Date", search=True), "description": fields.Text("Description", search=True), "weight": fields.Decimal("Weight"), "width": fields.Decimal("Width"), "length": fields.Decimal("Length"), "comments": fields.One2Many("message", "related_id", "Comments"), "product_id": fields.Many2One("product", "Product", search=True), "stock_balances": fields.One2Many("stock.balance", "lot_id", "Stock Quantities"), "service_item_id": fields.Many2One("service.item", "Service Item"), # XXX: deprecated } _order = "number desc" def _get_number(self, context={}): seq_id = get_model("sequence").find_sequence("stock_lot") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id) res = self.search([["number", "=", num]]) if not res: return num get_model("sequence").increment_number(seq_id) _defaults = { "number": _get_number, } def remove_expired_lots(self, context={}): print("StockLot.remove_expired_lots") access.set_active_user(1) access.set_active_company(1) settings = get_model("settings").browse(1) if not settings.lot_expiry_journal_id: raise Exception("Missing lot expiry journal") journal = settings.lot_expiry_journal_id if not journal.location_to_id: raise Exception("Missing to location in lot expiry journal") t = time.strftime("%Y-%m-%d") pick_vals = { "type": "out", "journal_id": journal.id, "lines": [], } n = 0 for obj in self.search_browse([["expiry_date", "<", t]]): prod = obj.product_id if not prod: continue for bal in obj.stock_balances: if bal.qty_phys <= 0: continue line_vals = { "product_id": prod.id, "location_from_id": bal.location_id.id, "location_to_id": journal.location_to_id.id, "lot_id": obj.id, "qty": bal.qty_phys, "uom_id": prod.uom_id.id, } pick_vals["lines"].append(("create", line_vals)) n += 1 if pick_vals["lines"]: pick_id = get_model("stock.picking").create( pick_vals, context={"pick_type": "out"}) get_model("stock.picking").set_done([pick_id]) get_model("stock.picking").trigger([pick_id], "lot_expired") return { "flash": "%d lots removed from stock" % n, }
class Coupon(Model): _name = "sale.coupon" _string = "Coupon" _key = ["code"] _fields = { "master_id": fields.Many2One("sale.coupon.master", "Coupon Master", required=True, search=True), "code": fields.Char("Code", required=True), "contact_id": fields.Many2One("contact", "Customer", required=True, search=True), "state": fields.Selection([["available", "Available"], ["in_use", "In Use"], ["used", "Used"], ["expired", "Expired"]], "Status", required=True, search=True), "active": fields.Boolean("Active", required=True), "use_date": fields.DateTime("Usage Date", readonly=True), "expiry_date": fields.DateTime("Expiry Date"), "use_duration": fields.Integer("Usability Duration (minutes)"), "hide_date": fields.DateTime("Hide Date"), "contact_email": fields.Char("Contact Email", function="_get_related", function_search="_search_related", function_context={"path": "contact_id.email"}, search=True), } def _get_code(self, context={}): while 1: code = "%.3d" % random.randint(0, 999) code += "-" code += "%.3d" % random.randint(0, 999) code += "-" code += "%.4d" % random.randint(0, 9999) if not get_model("sale.coupon").search([["code", "=", code]]): return code _defaults = { "state": "available", "active": True, "code": _get_code, } _order = "id desc" def use_coupon(self, ids, context={}): obj = self.browse(ids[0]) if obj.state != "available": raise Exception("Invalid coupon status") t = datetime.now() if obj.expiry_date and t.strftime( "%Y-%m-%d %H:%M:%S") >= obj.expiry_date: raise Exception("Coupon is expired") vals = { "state": "in_use", "use_date": t.strftime("%Y-%m-%d %H:%M:%S"), } if obj.use_duration: t2 = t + timedelta(minutes=obj.use_duration) vals["expiry_date"] = t2.strftime("%Y-%m-%d %H:%M:%S") obj.write(vals) def update_coupons(self, context={}): t = datetime.now().strftime("%Y-%m-%d %H:%M:%S") ids = self.search([["state", "=", "available"], ["expiry_date", "<=", t]]) if ids: self.write(ids, {"state": "expired"}) ids = self.search([["state", "=", "in_use"], ["expiry_date", "<=", t]]) if ids: self.write(ids, {"state": "used"}) ids = self.search([["active", "=", True], ["hide_date", "<=", t]]) if ids: self.write(ids, {"active": False}) #def get_report_data(self,ids,context={}): #objs = self.browse(ids) #for obj in objs: #if obj.state == "available": #t=datetime.now() #vals={ #"state": "used", #"use_date": t.strftime("%Y-%m-%d %H:%M:%S"), #"expiry_date": t.strftime("%Y-%m-%d %H:%M:%S"), #} #obj.write(vals) #data = super().get_report_data(ids, context) #return data def create_individual_coupon(self, master_ids=[], context={}): contact_ids = context.get("trigger_ids") for contact_id in contact_ids: for master_id in master_ids: master = get_model("sale.coupon.master").browse(master_id) vals = { "master_id": master_id, "contact_id": contact_id, "expiry_date": master.expiry_date, "use_duration": master.use_duration, "hide_date": master.hide_date, } self.create(vals)
class Leave(Model): _name = "hr.leave" _string = "Leave Request" _multi_company = True _key = ["number"] _fields = { "name": fields.Char("Name", function="get_name"), "number": fields.Char("Number"), "date": fields.Date("Request Date", required=True), "employee_id": fields.Many2One("hr.employee", "Employee", required=True), "employee_work_status": fields.Selection([["working", "Working"], ["dismissed", "Dismissed"], ["resigned", "Resigned"], ["died", "Died"]], "Work Status"), "leave_type_id": fields.Many2One("hr.leave.type", "Leave Type", required=True), "leave_reason": fields.Text("Leave Reason"), "start_date": fields.DateTime("Start Date", required=True), "end_date": fields.DateTime("End Date", required=True), "state": fields.Selection([["draft", "Draft"], ["waiting_approval", "Awaiting Approval"], ["approved", "Approved"], ["rejected", "Rejected"]], "Status"), "days_requested": fields.Decimal("Days Requested", function="cal_days_requested"), "days_remaining": fields.Decimal("Days Remaining", function="cal_days_remaining"), "documents": fields.One2Many("document", "related_id", "Documents"), "comments": fields.One2Many("message", "related_id", "Comments"), "time_from": fields.Char("From Time"), "time_to": fields.Char("To Time"), "period_id": fields.Many2One("hr.leave.period", "Leave Period", required=True), 'schedule_id': fields.Many2One("hr.schedule", "Working Schedule"), "user_id": fields.Many2One("base.user", "User"), "company_id": fields.Many2One("company", "Company"), } def _get_number(self, context={}): user_id = get_active_user() set_active_user(1) seq_id = get_model("sequence").find_sequence(type="leave_request") if not seq_id: return None while 1: num = get_model("sequence").get_next_number(seq_id) 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) set_active_user(user_id) def _get_employee(self, context={}): user_id = get_active_user() res = get_model("hr.employee").search([["user_id", "=", user_id]]) if not res: return None return res[0] _defaults = { "state": "draft", "date": lambda *a: time.strftime(FMT_DAY), "period_type": "day", "period_hour": 0, "number": _get_number, "employee_id": _get_employee, "company_id": lambda *a: get_active_company(), 'start_date': lambda *a: '%s 08:30:00' % time.strftime(FMT_DAY), 'end_date': lambda *a: '%s 18:00:00' % time.strftime(FMT_DAY), 'user_id': lambda *a: get_active_user(), } _order = "date desc, number desc" def get_name(self, ids, context={}): vals = {} for obj in self.browse(ids): emp = obj.employee_id vals[obj.id] = "%s %s" % (emp.first_name, emp.last_name) return vals def get_holidays(self, ids, context={}): date_from = context.get('date_from', time.strftime(FMT_DAY)) date_to = context.get('date_to', time.strftime(FMT_DAY)) cond = [ ['date', '>=', date_from], ['date', '<=', date_to], ] res = set() for r in get_model("hr.holiday").search_read(cond, ['date']): res.update({r['date']}) return list(res) def daterange(self, start_date, end_date): # interate between start date and stop date for n in range(int((end_date - start_date).days)): yield start_date + timedelta(n) def get_day_request(self, data={}): day_off = [5, 6] # Sat and Sun XXX default_hour = 8 total_break = 0 part1 = 0 employee_id = data.get('employee_id') if not employee_id: return 0 start_date = data.get('start_date') end_date = data.get('end_date') if not start_date or not end_date: return 0 holidays = get_model("hr.holiday").get_holidays(context=data) emp = get_model("hr.employee").browse(employee_id) start_date = datetime.strptime(start_date, FMT_TIME) end_date = datetime.strptime(end_date, FMT_TIME) total_day_off = 0 schedule = emp.schedule_id if data.get('schedule_id'): schedule = get_model("hr.schedule").browse(data['schedule_id']) if schedule: default_hour = schedule.working_hour or 0 total_break = schedule.total_break or 0 part1 = schedule.part1 or 0 for rdate in self.daterange(start_date, end_date + timedelta(days=1)): hdate = rdate.strftime(FMT_DAY) if hdate in holidays: total_day_off += 1 dow = rdate.weekday() if dow in day_off: total_day_off += 1 else: #items=schedule.get_day(context={'dow': dow}) pass total_day = 0 total_hrs = 0 total_day -= total_day_off if end_date >= start_date: diff = end_date - start_date total_day = diff.days total_day -= total_day_off total_sec = diff.seconds total_hrs = total_sec / 60 / 60 if total_hrs >= part1 and total_hrs <= part1 + total_break and part1 > 0: hrs2day = Decimal(0.5) else: if total_hrs >= part1 + total_break * 2: # XXX *2 total_hrs -= total_break hrs2day = (total_hrs / default_hour) res = total_day + hrs2day return Decimal(res) def cal_days_requested(self, ids, context={}): res = {} for obj in self.browse(ids): emp = obj.employee_id data = { 'employee_id': emp.id, 'start_date': obj.start_date, 'end_date': obj.end_date, } res[obj.id] = self.get_day_request(data) return res def cal_days_remaining(self, ids, context={}): vals = {} for obj in self.browse(ids): period_id = obj.period_id.id emp_id = obj.employee_id.id vals[obj.id] = self.get_remaining(emp_id, period_id) return vals def get_remaining(self, emp_id=None, period_id=None): if not emp_id or not period_id: return 0 requested = self.search_read( [["period_id", "=", period_id], ["state", "=", "approved"], ["employee_id", "=", emp_id]], ["days_requested"]) requested = sum([d.get("days_requested") or 0 for d in requested]) if period_id: total = get_model("hr.leave.period").read([period_id], ["max_days"])[0].get("max_days") or 0 else: total = 0 return total - requested 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"}) obj.trigger("submit_approval") def approve(self, ids, context={}): user_id = get_active_user() user = get_model("base.user").browse(user_id) res = get_model("hr.employee").search([["user_id", "=", user_id]]) if res: emp_id = res[0] for obj in self.browse(ids): if obj.state not in ("draft", "waiting_approval"): raise Exception("Invalid state") if res and obj.employee_id.id == emp_id: if obj.employee_id.approver_id: raise Exception("User %s is not authorized to approve his own leave requests" % user.name) else: if obj.employee_id.approver_id.id != user_id: raise Exception("User %s is not authorized to approve leave requests of employee %s" % (user.name, obj.employee_id.name_get()[0][1])) obj.write({"state": "approved"}) vals = { "related_id": "hr.leave,%s" % obj.id, "body": "Approved by %s" % user.name, } get_model("message").create(vals) obj.trigger("approved") def reject(self, ids, context={}): user_id = get_active_user() user = get_model("base.user").browse(user_id) res = get_model("hr.employee").search([["user_id", "=", user_id]]) if res: emp_id = res[0] for obj in self.browse(ids): if obj.state not in ("draft", "waiting_approval"): raise Exception("Invalid state") if res and obj.employee_id.id == emp_id: if obj.employee_id.approver_id: raise Exception("User %s is not authorized to reject his own leave requests" % user.name) else: if obj.employee_id.approver_id.id != user_id: raise Exception("User %s is not authorized to reject leave requests of employee %s" % (user.name, obj.employee_id.name_get()[0][1])) obj.write({"state": "rejected"}) vals = { "related_id": "hr.leave,%s" % obj.id, "body": "Rejected by %s" % user.name, } get_model("message").create(vals) obj.trigger("rejected") def do_reopen(self, ids, context={}): user_id = get_active_user() user = get_model("base.user").browse(user_id) res = get_model("hr.employee").search([["user_id", "=", user_id]]) if res: emp_id = res[0] for obj in self.browse(ids): assert obj.state in ("rejected", "approved") if res and obj.employee_id.id == emp_id: if obj.employee_id.approver_id: raise Exception("User %s is not authorized to reopen his own leave requests" % user.name) else: if obj.employee_id.approver_id.id != user_id: raise Exception("User %s is not authorized to reopen leave requests of employee %s" % (user.name, obj.employee_id.name_get()[0][1])) obj.write({"state": "waiting_approval"}) def to_draft(self, ids, context={}): for obj in self.browse(ids): obj.write({"state": "draft"}) def convert_date(self, date_in): date_out = [int(x) for x in date_in.split("-")] return date(date_out[0], date_out[1], date_out[2]) def write(self, ids, vals, **kw): obj = self.browse(ids)[0] vals['employee_work_status'] = obj.employee_id.work_status super().write(ids, vals, **kw) def onchange_date(self, context={}): data = context['data'] data['days_requested'] = self.get_day_request(data) return data def onchange_employee(self, context={}): data = context["data"] data["period_id"] = None data["leave_type_id"] = None data['days_remaining'] = None data['days_requested'] = None data['schedule_id'] = None employee_id = data['employee_id'] schd = get_model("hr.employee").browse(employee_id).schedule_id if schd: data['schedule_id'] = schd.id datenow = datetime.now() wd = datenow.weekday() res = [] for line in schd.lines: dow = int(line.dow or '0') - 1 if wd == dow: res.append({ 'time_start': '%s:00' % line.time_start, 'time_stop': '%s:00' % line.time_stop, }) if res: data['start_date'] = '%s %s' % (datenow.strftime(FMT_DAY), res[0]['time_start']) data['end_date'] = '%s %s' % (datenow.strftime(FMT_DAY), res[-1]['time_stop']) data['days_requested'] = self.get_day_request(data) return data def onchange_type(self, context={}): data = context["data"] leave_type = data["leave_type_id"] con = "leave_type_id = %s" % (leave_type) year = datetime.now().strftime("%Y") db = database.get_connection() pids = db.query("SELECT id FROM hr_leave_period WHERE %s" % con) pids = [pid["id"] for pid in pids] sids = [] for obj in get_model("hr.leave.period").browse(pids): if obj.get("date_to").find(year) != -1: sids.append(obj.id) data['days_remaining'] = obj.max_days or 0 if sids: pid = sids[-1] data["period_id"] = pid if data.get("employee_id"): data['days_remaining'] = self.get_remaining(data['employee_id'], pid) else: data["period_id"] = None return data