diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js index ef431d7d41a..8ec6a536269 100755 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.js +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js @@ -31,8 +31,7 @@ frappe.ui.form.on('POS Profile', { frm.set_query("print_format", function() { return { filters: [ - ['Print Format', 'doc_type', '=', 'Sales Invoice'], - ['Print Format', 'print_format_type', '=', 'Jinja'], + ['Print Format', 'doc_type', '=', 'POS Invoice'] ] }; }); @@ -45,10 +44,6 @@ frappe.ui.form.on('POS Profile', { }; }); - frm.set_query("print_format", function() { - return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} }; - }); - frm.set_query('company_address', function(doc) { if(!doc.company) { frappe.throw(__('Please set Company')); diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/__init__.py b/erpnext/accounts/doctype/process_statement_of_accounts/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html new file mode 100644 index 00000000000..e1ddeff61f7 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html @@ -0,0 +1,89 @@ +

{{ filters.party[0] }}

+

{{ _("Statement of Accounts") }}

+ +
+ {{ frappe.format(filters.from_date, 'Date')}} + {{ _("to") }} + {{ frappe.format(filters.to_date, 'Date')}} +
+ + + + + + + + + + + + + + {% for row in data %} + + {% if(row.posting_date) %} + + + + + + {% else %} + + + + + + {% endif %} + + + {% endfor %} + +
{{ _("Date") }}{{ _("Ref") }}{{ _("Party") }}{{ _("Debit") }}{{ _("Credit") }}{{ _("Balance (Dr - Cr)") }}
{{ frappe.format(row.posting_date, 'Date') }}{{ row.voucher_type }} +
{{ row.voucher_no }}
+ {% if not (filters.party or filters.account) %} + {{ row.party or row.account }} +
+ {% endif %} + + {{ _("Against") }}: {{ row.against }} +
{{ _("Remarks") }}: {{ row.remarks }} + {% if row.bill_no %} +
{{ _("Supplier Invoice No") }}: {{ row.bill_no }} + {% endif %} +
+ {{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }} + {{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }} + {{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }} + + {{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }} + + {{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }} +
+

+{% if aging %} +

{{ _("Ageing Report Based On ") }} {{ aging.ageing_based_on }}

+
+ {{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}} +
+
+ + + + + + + + + + + + + + + + + + +
30 Days60 Days90 Days120 Days
{{ aging.range1 }}{{ aging.range2 }}{{ aging.range3 }}{{ aging.range4 }}
+{% endif %} +

Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}

\ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js new file mode 100644 index 00000000000..7425132c468 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js @@ -0,0 +1,132 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Process Statement Of Accounts', { + view_properties: function(frm) { + frappe.route_options = {doc_type: 'Customer'}; + frappe.set_route("Form", "Customize Form"); + }, + refresh: function(frm){ + if(!frm.doc.__islocal) { + frm.add_custom_button('Send Emails',function(){ + frappe.call({ + method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_emails", + args: { + "document_name": frm.doc.name, + }, + callback: function(r) { + if(r && r.message) { + frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'}); + } + else{ + frappe.msgprint('No Records for these settings.') + } + } + }); + }); + frm.add_custom_button('Download',function(){ + var url = frappe.urllib.get_full_url( + '/api/method/erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.download_statements?' + + 'document_name='+encodeURIComponent(frm.doc.name)) + $.ajax({ + url: url, + type: 'GET', + success: function(result) { + if(jQuery.isEmptyObject(result)){ + frappe.msgprint('No Records for these settings.'); + } + else{ + window.location = url; + } + } + }); + }); + } + }, + onload: function(frm) { + frm.set_query('currency', function(){ + return { + filters: { + 'enabled': 1 + } + } + }); + if(frm.doc.__islocal){ + frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); + frm.set_value('to_date', frappe.datetime.get_today()); + } + }, + customer_collection: function(frm){ + frm.set_value('collection_name', ''); + if(frm.doc.customer_collection){ + frm.get_field('collection_name').set_label(frm.doc.customer_collection); + } + }, + frequency: function(frm){ + if(frm.doc.frequency != ''){ + frm.set_value('start_date', frappe.datetime.get_today()); + } + else{ + frm.set_value('start_date', ''); + } + }, + fetch_customers: function(frm){ + if(frm.doc.collection_name){ + frappe.call({ + method: "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.fetch_customers", + args: { + 'customer_collection': frm.doc.customer_collection, + 'collection_name': frm.doc.collection_name, + 'primary_mandatory': frm.doc.primary_mandatory + }, + callback: function(r) { + if(!r.exc) { + if(r.message.length){ + frm.clear_table('customers'); + for (const customer of r.message){ + var row = frm.add_child('customers'); + row.customer = customer.name; + row.primary_email = customer.primary_email; + row.billing_email = customer.billing_email; + } + frm.refresh_field('customers'); + } + else{ + frappe.msgprint('No Customers found with selected options.'); + } + } + } + }); + } + else { + frappe.throw('Enter ' + frm.doc.customer_collection + ' name.'); + } + } +}); + +frappe.ui.form.on('Process Statement Of Accounts Customer', { + customer: function(frm, cdt, cdn){ + var row = locals[cdt][cdn]; + if (!row.customer){ + return; + } + frappe.call({ + method: 'erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.get_customer_emails', + args: { + 'customer_name': row.customer, + 'primary_mandatory': frm.doc.primary_mandatory + }, + callback: function(r){ + if(!r.exe){ + if(r.message.length){ + frappe.model.set_value(cdt, cdn, "primary_email", r.message[0]) + frappe.model.set_value(cdt, cdn, "billing_email", r.message[1]) + } + else { + return + } + } + } + }) + } +}); \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json new file mode 100644 index 00000000000..4be0e2ec068 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json @@ -0,0 +1,310 @@ +{ + "actions": [], + "allow_workflow": 1, + "autoname": "Prompt", + "creation": "2020-05-22 16:46:18.712954", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "section_break_11", + "from_date", + "company", + "account", + "group_by", + "cost_center", + "column_break_14", + "to_date", + "finance_book", + "currency", + "project", + "section_break_3", + "customer_collection", + "collection_name", + "fetch_customers", + "column_break_6", + "primary_mandatory", + "column_break_17", + "customers", + "preferences", + "orientation", + "section_break_14", + "include_ageing", + "ageing_based_on", + "section_break_1", + "enable_auto_email", + "section_break_18", + "frequency", + "filter_duration", + "column_break_21", + "start_date", + "section_break_33", + "subject", + "column_break_28", + "cc_to", + "section_break_30", + "body", + "help_text" + ], + "fields": [ + { + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Frequency", + "options": "Weekly\nMonthly\nQuarterly" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "depends_on": "eval:doc.enable_auto_email == 0;", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "mandatory_depends_on": "eval:doc.frequency == '';" + }, + { + "depends_on": "eval:doc.enable_auto_email == 0;", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "mandatory_depends_on": "eval:doc.frequency == '';" + }, + { + "fieldname": "cost_center", + "fieldtype": "Table MultiSelect", + "label": "Cost Center", + "options": "PSOA Cost Center" + }, + { + "fieldname": "project", + "fieldtype": "Table MultiSelect", + "label": "Project", + "options": "PSOA Project" + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Customers" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break", + "label": "General Ledger Filters" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_17", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "customer_collection", + "fieldtype": "Select", + "label": "Select Customers By", + "options": "\nCustomer Group\nTerritory\nSales Partner\nSales Person" + }, + { + "depends_on": "eval: doc.customer_collection !== ''", + "fieldname": "collection_name", + "fieldtype": "Dynamic Link", + "label": "Recipient", + "options": "customer_collection" + }, + { + "fieldname": "section_break_1", + "fieldtype": "Section Break", + "label": "Email Settings" + }, + { + "fieldname": "account", + "fieldtype": "Link", + "label": "Account", + "options": "Account" + }, + { + "fieldname": "finance_book", + "fieldtype": "Link", + "label": "Finance Book", + "options": "Finance Book" + }, + { + "fieldname": "preferences", + "fieldtype": "Section Break", + "label": "Print Preferences" + }, + { + "fieldname": "orientation", + "fieldtype": "Select", + "label": "Orientation", + "options": "Landscape\nPortrait" + }, + { + "default": "Today", + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date" + }, + { + "default": "Group by Voucher (Consolidated)", + "fieldname": "group_by", + "fieldtype": "Select", + "label": "Group By", + "options": "\nGroup by Voucher\nGroup by Voucher (Consolidated)" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "options": "Currency" + }, + { + "default": "0", + "fieldname": "include_ageing", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Include Ageing Summary" + }, + { + "default": "Due Date", + "depends_on": "eval:doc.include_ageing === 1", + "fieldname": "ageing_based_on", + "fieldtype": "Select", + "label": "Ageing Based On", + "options": "Due Date\nPosting Date" + }, + { + "default": "0", + "fieldname": "enable_auto_email", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Enable Auto Email" + }, + { + "fieldname": "section_break_14", + "fieldtype": "Column Break", + "hide_border": 1 + }, + { + "depends_on": "eval: doc.enable_auto_email ==1", + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_21", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.customer_collection !== ''", + "fieldname": "fetch_customers", + "fieldtype": "Button", + "label": "Fetch Customers", + "options": "fetch_customers", + "print_hide": 1, + "report_hide": 1 + }, + { + "default": "1", + "fieldname": "primary_mandatory", + "fieldtype": "Check", + "label": "Send To Primary Contact" + }, + { + "fieldname": "cc_to", + "fieldtype": "Link", + "label": "CC To", + "options": "User" + }, + { + "default": "1", + "fieldname": "filter_duration", + "fieldtype": "Int", + "label": "Filter Duration (Months)" + }, + { + "fieldname": "customers", + "fieldtype": "Table", + "label": "Customers", + "options": "Process Statement Of Accounts Customer", + "reqd": 1 + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_30", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "help_text", + "fieldtype": "HTML", + "label": "Help Text", + "options": "
\n

Note

\n\n

Examples

\n\n\n" + }, + { + "fieldname": "subject", + "fieldtype": "Data", + "label": "Subject" + }, + { + "fieldname": "body", + "fieldtype": "Text Editor", + "label": "Body" + } + ], + "links": [], + "modified": "2020-08-08 08:47:09.185728", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Statement Of Accounts", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py new file mode 100644 index 00000000000..d50e4a8af95 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py @@ -0,0 +1,271 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from erpnext.accounts.report.general_ledger.general_ledger import execute as get_soa +from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute as get_ageing +from frappe.core.doctype.communication.email import make + +from frappe.utils.print_format import report_to_pdf +from frappe.utils.pdf import get_pdf +from frappe.utils import today, add_days, add_months, getdate, format_date +from frappe.utils.jinja import validate_template + +import copy +from datetime import timedelta +from frappe.www.printview import get_print_style + +class ProcessStatementOfAccounts(Document): + def validate(self): + if not self.subject: + self.subject = 'Statement Of Accounts for {{ customer.name }}' + if not self.body: + self.body = 'Hello {{ customer.name }},
PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.' + + validate_template(self.subject) + validate_template(self.body) + + if not self.customers: + frappe.throw(frappe._('Customers not selected.')) + + if self.enable_auto_email: + self.to_date = self.start_date + self.from_date = add_months(self.to_date, -1 * self.filter_duration) + + +def get_report_pdf(doc, consolidated=True): + statement_dict = {} + aging = '' + base_template_path = "frappe/www/printview.html" + template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + + for entry in doc.customers: + if doc.include_ageing: + ageing_filters = frappe._dict({ + 'company': doc.company, + 'report_date': doc.to_date, + 'ageing_based_on': doc.ageing_based_on, + 'range1': 30, + 'range2': 60, + 'range3': 90, + 'range4': 120, + 'customer': entry.customer + }) + col1, aging = get_ageing(ageing_filters) + aging[0]['ageing_based_on'] = doc.ageing_based_on + + tax_id = frappe.get_doc('Customer', entry.customer).tax_id + + filters= frappe._dict({ + 'from_date': doc.from_date, + 'to_date': doc.to_date, + 'company': doc.company, + 'finance_book': doc.finance_book if doc.finance_book else None, + "account": doc.account if doc.account else None, + 'party_type': 'Customer', + 'party': [entry.customer], + 'group_by': doc.group_by, + 'currency': doc.currency, + 'cost_center': [cc.cost_center_name for cc in doc.cost_center], + 'project': [p.project_name for p in doc.project], + 'show_opening_entries': 0, + 'include_default_book_entries': 0, + 'show_cancelled_entries': 1, + 'tax_id': tax_id if tax_id else None + }) + col, res = get_soa(filters) + + for x in [0, -2, -1]: + res[x]['account'] = res[x]['account'].replace("'","") + + if len(res) == 3: + continue + html = frappe.render_template(template_path, \ + {"filters": filters, "data": res, "aging": aging[0] if doc.include_ageing else None}) + html = frappe.render_template(base_template_path, {"body": html, \ + "css": get_print_style(), "title": "Statement For " + entry.customer}) + statement_dict[entry.customer] = html + if not bool(statement_dict): + return False + elif consolidated: + result = ''.join(list(statement_dict.values())) + return get_pdf(result, {'orientation': doc.orientation}) + else: + for customer, statement_html in statement_dict.items(): + statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation}) + return statement_dict + +def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name): + fields_dict = { + 'Customer Group': 'customer_group', + 'Territory': 'territory', + } + collection = frappe.get_doc(customer_collection, collection_name) + selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[ + ['lft', '>=', collection.lft], + ['rgt', '<=', collection.rgt] + ], + fields=['name'], + order_by='lft asc, rgt desc' + )] + return frappe.get_list('Customer', fields=['name', 'email_id'], \ + filters=[[fields_dict[customer_collection], 'IN', selected]]) + +def get_customers_based_on_sales_person(sales_person): + lft, rgt = frappe.db.get_value("Sales Person", + sales_person, ["lft", "rgt"]) + records = frappe.db.sql(""" + select distinct parent, parenttype + from `tabSales Team` steam + where parenttype = 'Customer' + and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) + """, (lft, rgt), as_dict=1) + sales_person_records = frappe._dict() + for d in records: + sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ + filters=[['name', 'in', list(sales_person_records['Customer'])]]) + return customers + +def get_recipients_and_cc(customer, doc): + recipients = [] + for clist in doc.customers: + if clist.customer == customer: + recipients.append(clist.billing_email) + if doc.primary_mandatory and clist.primary_email: + recipients.append(clist.primary_email) + cc = [] + if doc.cc_to != '': + try: + cc=[frappe.get_value('User', doc.cc_to, 'email')] + except: + pass + + return recipients, cc + +def get_context(customer, doc): + template_doc = copy.deepcopy(doc) + del template_doc.customers + template_doc.from_date = format_date(template_doc.from_date) + template_doc.to_date = format_date(template_doc.to_date) + return { + 'doc': template_doc, + 'customer': frappe.get_doc('Customer', customer), + 'frappe': frappe.utils + } + +@frappe.whitelist() +def fetch_customers(customer_collection, collection_name, primary_mandatory): + customer_list = [] + customers = [] + + if customer_collection == 'Sales Person': + customers = get_customers_based_on_sales_person(collection_name) + if not bool(customers): + frappe.throw('No Customers found with selected options.') + else: + if customer_collection == 'Sales Partner': + customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ + filters=[['default_sales_partner', '=', collection_name]]) + else: + customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name) + + for customer in customers: + primary_email = customer.get('email_id') or '' + billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False) + + if billing_email == '' or (primary_email == '' and int(primary_mandatory)): + continue + + customer_list.append({ + 'name': customer.name, + 'primary_email': primary_email, + 'billing_email': billing_email + }) + return customer_list + +@frappe.whitelist() +def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): + billing_email = frappe.db.sql(""" + SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent \ + WHERE l.link_doctype='Customer' and l.link_name='""" + customer_name + """' and \ + c.is_billing_contact=1 \ + order by c.creation desc""") + + if len(billing_email) == 0 or (billing_email[0][0] is None): + if billing_and_primary: + frappe.throw('No billing email found for customer: '+ customer_name) + else: + return '' + + if billing_and_primary: + primary_email = frappe.get_value('Customer', customer_name, 'email_id') + if primary_email is None and int(primary_mandatory): + frappe.throw('No primary email found for customer: '+ customer_name) + return [primary_email or '', billing_email[0][0]] + else: + return billing_email[0][0] or '' + +@frappe.whitelist() +def download_statements(document_name): + doc = frappe.get_doc('Process Statement Of Accounts', document_name) + report = get_report_pdf(doc) + if report: + frappe.local.response.filename = doc.name + '.pdf' + frappe.local.response.filecontent = report + frappe.local.response.type = "download" + +@frappe.whitelist() +def send_emails(document_name, from_scheduler=False): + doc = frappe.get_doc('Process Statement Of Accounts', document_name) + report = get_report_pdf(doc, consolidated=False) + + if report: + for customer, report_pdf in report.items(): + attachments = [{ + 'fname': customer + '.pdf', + 'fcontent': report_pdf + }] + + recipients, cc = get_recipients_and_cc(customer, doc) + context = get_context(customer, doc) + subject = frappe.render_template(doc.subject, context) + message = frappe.render_template(doc.body, context) + + frappe.enqueue( + queue='short', + method=frappe.sendmail, + recipients=recipients, + sender=frappe.session.user, + cc=cc, + subject=subject, + message=message, + now=True, + reference_doctype='Process Statement Of Accounts', + reference_name=document_name, + attachments=attachments + ) + + if doc.enable_auto_email and from_scheduler: + new_to_date = getdate(today()) + if doc.frequency == 'Weekly': + new_to_date = add_days(new_to_date, 7) + else: + new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3) + new_from_date = add_months(new_to_date, -1 * doc.filter_duration) + doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now())) + doc.db_set('to_date', new_to_date, commit=True) + doc.db_set('from_date', new_from_date, commit=True) + return True + else: + return False + +@frappe.whitelist() +def send_auto_email(): + selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1}) + for entry in selected: + send_emails(entry.name, from_scheduler=True) + return True \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py new file mode 100644 index 00000000000..30efbb36833 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts/test_process_statement_of_accounts.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestProcessStatementOfAccounts(unittest.TestCase): + pass diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py b/erpnext/accounts/doctype/process_statement_of_accounts_customer/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json new file mode 100644 index 00000000000..dd04dc1b3c6 --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.json @@ -0,0 +1,47 @@ +{ + "actions": [], + "allow_workflow": 1, + "creation": "2020-08-03 16:35:21.852178", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer", + "billing_email", + "primary_email" + ], + "fields": [ + { + "fieldname": "customer", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Customer", + "options": "Customer", + "reqd": 1 + }, + { + "fieldname": "primary_email", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Primary Contact Email" + }, + { + "fieldname": "billing_email", + "fieldtype": "Read Only", + "in_list_view": 1, + "label": "Billing Email" + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-03 22:55:38.875601", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Process Statement Of Accounts Customer", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py new file mode 100644 index 00000000000..1a760101dba --- /dev/null +++ b/erpnext/accounts/doctype/process_statement_of_accounts_customer/process_statement_of_accounts_customer.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class ProcessStatementOfAccountsCustomer(Document): + pass diff --git a/erpnext/accounts/doctype/psoa_cost_center/__init__.py b/erpnext/accounts/doctype/psoa_cost_center/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json new file mode 100644 index 00000000000..e292b60d68d --- /dev/null +++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-08-03 16:56:45.744905", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cost_center_name" + ], + "fields": [ + { + "fieldname": "cost_center_name", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-03 16:56:45.744905", + "modified_by": "Administrator", + "module": "Accounts", + "name": "PSOA Cost Center", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py new file mode 100644 index 00000000000..0aeef3ed3a8 --- /dev/null +++ b/erpnext/accounts/doctype/psoa_cost_center/psoa_cost_center.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PSOACostCenter(Document): + pass diff --git a/erpnext/accounts/doctype/psoa_project/__init__.py b/erpnext/accounts/doctype/psoa_project/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/doctype/psoa_project/psoa_project.json b/erpnext/accounts/doctype/psoa_project/psoa_project.json new file mode 100644 index 00000000000..20a03eed96e --- /dev/null +++ b/erpnext/accounts/doctype/psoa_project/psoa_project.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-08-03 16:52:14.731978", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "project_name" + ], + "fields": [ + { + "fieldname": "project_name", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-03 16:53:39.219736", + "modified_by": "Administrator", + "module": "Accounts", + "name": "PSOA Project", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/psoa_project/psoa_project.py b/erpnext/accounts/doctype/psoa_project/psoa_project.py new file mode 100644 index 00000000000..f4a5dee9752 --- /dev/null +++ b/erpnext/accounts/doctype/psoa_project/psoa_project.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class PSOAProject(Document): + pass diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 2e91c8ef19f..d62e73b6ac6 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -180,7 +180,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "ACC-PINV-.YYYY.-", + "options": "ACC-PINV-.YYYY.-\nACC-PINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -1334,7 +1334,7 @@ "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-08-03 12:46:01.411074", + "modified": "2020-08-03 23:20:04.466153", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1396,4 +1396,4 @@ "timeline_field": "supplier", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 4dc81e90875..31613e50b04 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_workflow": 1, "autoname": "naming_series:", "creation": "2013-05-24 19:29:05", "doctype": "DocType", @@ -217,7 +216,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "ACC-SINV-.YYYY.-", + "options": "ACC-SINV-.YYYY.-\nACC-SINV-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -1947,7 +1946,7 @@ "idx": 181, "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:07:16.725974", + "modified": "2020-08-03 23:31:12.675040", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 964566a17ef..9660c9570e2 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -206,10 +206,19 @@ class TestSalesInvoice(unittest.TestCase): "rate": 14, 'included_in_print_rate': 1 }) + si.append("taxes", { + "charge_type": "On Item Quantity", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CESS", + "rate": 5, + 'included_in_print_rate': 1 + }) si.insert() # with inclusive tax - self.assertEqual(si.net_total, 4385.96) + self.assertEqual(si.items[0].net_amount, 3947.368421052631) + self.assertEqual(si.net_total, 3947.37) self.assertEqual(si.grand_total, 5000) si.reload() @@ -222,8 +231,8 @@ class TestSalesInvoice(unittest.TestCase): si.save() # with inclusive tax and additional discount - self.assertEqual(si.net_total, 4285.96) - self.assertEqual(si.grand_total, 4885.99) + self.assertEqual(si.net_total, 3847.37) + self.assertEqual(si.grand_total, 4886) si.reload() @@ -235,7 +244,7 @@ class TestSalesInvoice(unittest.TestCase): si.save() # with inclusive tax and additional discount - self.assertEqual(si.net_total, 4298.25) + self.assertEqual(si.net_total, 3859.65) self.assertEqual(si.grand_total, 4900.00) def test_sales_invoice_discount_amount(self): diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py index 0e9c808608b..d825c6fd325 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py @@ -8,7 +8,7 @@ def get_data(): 'fieldname': 'taxes_and_charges', 'non_standard_fieldnames': { 'Tax Rule': 'sales_tax_template', - 'Subscription': 'tax_template', + 'Subscription': 'sales_tax_template', 'Restaurant': 'default_tax_template' }, 'transactions': [ diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 28a65196502..2f800bb2abd 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -611,7 +611,7 @@ def get_partywise_advanced_payment_amount(party_type, posting_date = None, futur cond = "posting_date <= '{0}'".format(posting_date) if company: - cond += "and company = '{0}'".format(company) + cond += "and company = {0}".format(frappe.db.escape(company)) data = frappe.db.sql(""" SELECT party, sum({0}) as amount FROM `tabGL Entry` diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py index 148357f3921..34facd8d050 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.py @@ -11,7 +11,7 @@ from erpnext.assets.doctype.asset_maintenance.asset_maintenance import calculate class AssetMaintenanceLog(Document): def validate(self): - if getdate(self.due_date) < getdate(nowdate()): + if getdate(self.due_date) < getdate(nowdate()) and self.maintenance_status not in ["Completed", "Cancelled"]: self.maintenance_status = "Overdue" if self.maintenance_status == "Completed" and not self.completion_date: diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js index b854413310a..23000e60eff 100644 --- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js +++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log_list.js @@ -1,14 +1,15 @@ frappe.listview_settings['Asset Maintenance Log'] = { add_fields: ["maintenance_status"], + has_indicator_for_draft: 1, get_indicator: function(doc) { - if(doc.maintenance_status=="Pending") { - return [__("Pending"), "orange"]; - } else if(doc.maintenance_status=="Completed") { - return [__("Completed"), "green"]; - } else if(doc.maintenance_status=="Cancelled") { - return [__("Cancelled"), "red"]; - } else if(doc.maintenance_status=="Overdue") { - return [__("Overdue"), "red"]; + if (doc.maintenance_status=="Planned") { + return [__(doc.maintenance_status), "orange", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Completed") { + return [__(doc.maintenance_status), "green", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Cancelled") { + return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; + } else if (doc.maintenance_status=="Overdue") { + return [__(doc.maintenance_status), "red", "status,=," + doc.maintenance_status]; } } }; diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 155597e8565..fd702c74c73 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -8,6 +8,7 @@ from frappe import _ from frappe.utils import flt, getdate, cint, date_diff, formatdate from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from frappe.model.document import Document +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts class AssetValueAdjustment(Document): def validate(self): @@ -53,17 +54,33 @@ class AssetValueAdjustment(Document): je.company = self.company je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount) - je.append("accounts", { + credit_entry = { "account": accumulated_depreciation_account, "credit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center - }) + } - je.append("accounts", { + debit_entry = { "account": depreciation_expense_account, "debit_in_account_currency": self.difference_amount, "cost_center": depreciation_cost_center or self.cost_center - }) + } + + accounting_dimensions = get_checks_for_pl_and_bs_accounts() + + for dimension in accounting_dimensions: + if dimension.get('mandatory_for_bs'): + credit_entry.update({ + dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') + }) + + if dimension.get('mandatory_for_pl'): + debit_entry.update({ + dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension') + }) + + je.append("accounts", credit_entry) + je.append("accounts", debit_entry) je.flags.ignore_permissions = True je.submit() diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 25065ab155f..9f2b9714f74 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -94,7 +94,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) { this.frm.add_custom_button(__('Update Items'), () => { erpnext.utils.update_child_items({ - frm: frm, + frm: this.frm, child_docname: "items", child_doctype: "Purchase Order Detail", cannot_add_row: false, diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 89c38c710b4..3091193b8dc 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -479,7 +479,11 @@ class AccountsController(TransactionBase): if d.against_order: allocated_amount = flt(d.amount) else: - amount = self.rounded_total or self.grand_total + if self.get('party_account_currency') == self.company_currency: + amount = self.get('base_rounded_total') or self.base_grand_total + else: + amount = self.get('rounded_total') or self.grand_total + allocated_amount = min(amount - advance_allocated, d.amount) advance_allocated += flt(allocated_amount) @@ -802,10 +806,22 @@ class AccountsController(TransactionBase): self.payment_terms_template = '' return + party_account_currency = self.get('party_account_currency') + if not party_account_currency: + party_type, party = self.get_party() + + if party_type and party: + party_account_currency = get_party_account_currency(party_type, party, self.company) + posting_date = self.get("bill_date") or self.get("posting_date") or self.get("transaction_date") date = self.get("due_date") due_date = date or posting_date - grand_total = self.get("rounded_total") or self.grand_total + + if party_account_currency == self.company_currency: + grand_total = self.get("base_rounded_total") or self.base_grand_total + else: + grand_total = self.get("rounded_total") or self.grand_total + if self.doctype in ("Sales Invoice", "Purchase Invoice"): grand_total = grand_total - flt(self.write_off_amount) @@ -850,13 +866,25 @@ class AccountsController(TransactionBase): def validate_payment_schedule_amount(self): if self.doctype == 'Sales Invoice' and self.is_pos: return + party_account_currency = self.get('party_account_currency') + if not party_account_currency: + party_type, party = self.get_party() + + if party_type and party: + party_account_currency = get_party_account_currency(party_type, party, self.company) + if self.get("payment_schedule"): total = 0 for d in self.get("payment_schedule"): total += flt(d.payment_amount) - total = flt(total, self.precision("grand_total")) - grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total')) + if party_account_currency == self.company_currency: + total = flt(total, self.precision("base_grand_total")) + grand_total = flt(self.get("base_rounded_total") or self.base_grand_total, self.precision('base_grand_total')) + else: + total = flt(total, self.precision("grand_total")) + grand_total = flt(self.get("rounded_total") or self.grand_total, self.precision('grand_total')) + if self.get("total_advance"): grand_total -= self.get("total_advance") @@ -957,7 +985,7 @@ def validate_inclusive_tax(tax, doc): # all rows about the reffered tax should be inclusive _on_previous_row_error("1 - %d" % (tax.row_id,)) elif tax.get("category") == "Valuation": - frappe.throw(_("Valuation type charges can not marked as Inclusive")) + frappe.throw(_("Valuation type charges can not be marked as Inclusive")) def set_balance_in_account_currency(gl_dict, account_currency=None, conversion_rate=None, company_currency=None): diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index babc5bdd797..37b7e31e611 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -613,9 +613,12 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters): if not taxes: return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """) else: + valid_from = filters.get('valid_from') + valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from + args = { 'item_code': filters.get('item_code'), - 'posting_date': filters.get('valid_from'), + 'posting_date': valid_from, 'tax_category': filters.get('tax_category'), 'company': filters.get('company') } diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 572e1ca2397..8f86dce4360 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -161,8 +161,9 @@ class calculate_taxes_and_totals(object): for item in self.doc.get("items"): item_tax_map = self._load_item_tax_rate(item.item_tax_rate) cumulated_tax_fraction = 0 + total_inclusive_tax_amount_per_qty = 0 for i, tax in enumerate(self.doc.get("taxes")): - tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax, item_tax_map) + tax.tax_fraction_for_current_item, inclusive_tax_amount_per_qty = self.get_current_tax_fraction(tax, item_tax_map) if i==0: tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item @@ -172,9 +173,12 @@ class calculate_taxes_and_totals(object): + tax.tax_fraction_for_current_item cumulated_tax_fraction += tax.tax_fraction_for_current_item + total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.stock_qty) - if cumulated_tax_fraction and not self.discount_amount_applied and item.qty: - item.net_amount = flt(item.amount / (1 + cumulated_tax_fraction)) + if not self.discount_amount_applied and item.qty and (cumulated_tax_fraction or total_inclusive_tax_amount_per_qty): + amount = flt(item.amount) - total_inclusive_tax_amount_per_qty + + item.net_amount = flt(amount / (1 + cumulated_tax_fraction)) item.net_rate = flt(item.net_amount / item.qty, item.precision("net_rate")) item.discount_percentage = flt(item.discount_percentage, item.precision("discount_percentage")) @@ -190,6 +194,7 @@ class calculate_taxes_and_totals(object): from tax inclusive amount """ current_tax_fraction = 0 + inclusive_tax_amount_per_qty = 0 if cint(tax.included_in_print_rate): tax_rate = self._get_tax_rate(tax, item_tax_map) @@ -204,10 +209,15 @@ class calculate_taxes_and_totals(object): elif tax.charge_type == "On Previous Row Total": current_tax_fraction = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item + + elif tax.charge_type == "On Item Quantity": + inclusive_tax_amount_per_qty = flt(tax_rate) - if getattr(tax, "add_deduct_tax", None): - current_tax_fraction *= -1.0 if (tax.add_deduct_tax == "Deduct") else 1.0 - return current_tax_fraction + if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct": + current_tax_fraction *= -1.0 + inclusive_tax_amount_per_qty *= -1.0 + + return current_tax_fraction, inclusive_tax_amount_per_qty def _get_tax_rate(self, tax, item_tax_map): if tax.account_head in item_tax_map: @@ -321,7 +331,7 @@ class calculate_taxes_and_totals(object): current_tax_amount = (tax_rate / 100.0) * \ self.doc.get("taxes")[cint(tax.row_id) - 1].grand_total_for_current_item elif tax.charge_type == "On Item Quantity": - current_tax_amount = tax_rate * item.stock_qty + current_tax_amount = tax_rate * item.qty self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) @@ -472,7 +482,7 @@ class calculate_taxes_and_totals(object): actual_taxes_dict = {} for tax in self.doc.get("taxes"): - if tax.charge_type == "Actual": + if tax.charge_type in ["Actual", "On Item Quantity"]: tax_amount = self.get_tax_amount_if_for_valuation_or_deduction(tax.tax_amount, tax) actual_taxes_dict.setdefault(tax.idx, tax_amount) elif tax.row_id in actual_taxes_dict: diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 545e2324acf..5cd5233b2e4 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -16,6 +16,7 @@ "opportunity_from", "party_name", "customer_name", + "source", "column_break0", "title", "opportunity_type", @@ -49,10 +50,9 @@ "contact_email", "contact_mobile", "more_info", - "source", + "company", "campaign", "column_break1", - "company", "transaction_date", "amended_from", "lost_reasons" @@ -344,7 +344,7 @@ "collapsible": 1, "fieldname": "more_info", "fieldtype": "Section Break", - "label": "Source", + "label": "More Information", "oldfieldtype": "Section Break", "options": "fa fa-file-text" }, @@ -424,7 +424,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2020-07-14 16:49:15.888503", + "modified": "2020-08-11 17:34:35.066961", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 1b071ea1b70..e152850f170 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -119,11 +119,19 @@ class Opportunity(TransactionBase): and q.status not in ('Lost', 'Closed')""", self.name) def has_ordered_quotation(self): - return frappe.db.sql(""" - select q.name - from `tabQuotation` q, `tabQuotation Item` qi - where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s - and q.status = 'Ordered'""", self.name) + if not self.with_items: + return frappe.get_all('Quotation', + { + 'opportunity': self.name, + 'status': 'Ordered', + 'docstatus': 1 + }, 'name') + else: + return frappe.db.sql(""" + select q.name + from `tabQuotation` q, `tabQuotation Item` qi + where q.name = qi.parent and q.docstatus=1 and qi.prevdoc_docname =%s + and q.status = 'Ordered'""", self.name) def has_lost_quotation(self): lost_quotation = frappe.db.sql(""" @@ -330,7 +338,7 @@ def make_opportunity_from_communication(communication, ignore_communication_link opportunity = frappe.get_doc({ "doctype": "Opportunity", "opportunity_from": opportunity_from, - "lead": lead + "party_name": lead }).insert(ignore_permissions=True) link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links) diff --git a/erpnext/education/doctype/fees/test_fees.py b/erpnext/education/doctype/fees/test_fees.py index b182992922b..eedc2ae7301 100644 --- a/erpnext/education/doctype/fees/test_fees.py +++ b/erpnext/education/doctype/fees/test_fees.py @@ -7,7 +7,7 @@ import frappe import unittest from frappe.utils import nowdate from frappe.utils.make_random import get_random - +from erpnext.education.doctype.program.test_program import make_program_and_linked_courses # test_records = frappe.get_test_records('Fees') @@ -15,6 +15,7 @@ class TestFees(unittest.TestCase): def test_fees(self): student = get_random("Student") + program = make_program_and_linked_courses("_Test Program 1", ["_Test Course 1", "_Test Course 2"]) fee = frappe.new_doc("Fees") fee.posting_date = nowdate() fee.due_date = nowdate() @@ -23,6 +24,7 @@ class TestFees(unittest.TestCase): fee.income_account = "Sales - _TC" fee.cost_center = "_Test Cost Center - _TC" fee.company = "_Test Company" + fee.program = program.name fee.extend("components", [ { diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js index 971e166067e..60f0f9d56d6 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.js @@ -134,7 +134,7 @@ let transfer_patient_dialog = function(frm) { {fieldtype: 'Link', label: 'Leave From', fieldname: 'leave_from', options: 'Healthcare Service Unit', reqd: 1, read_only:1}, {fieldtype: 'Link', label: 'Service Unit Type', fieldname: 'service_unit_type', options: 'Healthcare Service Unit Type'}, {fieldtype: 'Link', label: 'Transfer To', fieldname: 'service_unit', options: 'Healthcare Service Unit', reqd: 1}, - {fieldtype: 'Datetime', label: 'Check In', fieldname: 'check_in', reqd: 1} + {fieldtype: 'Datetime', label: 'Check In', fieldname: 'check_in', reqd: 1, default: frappe.datetime.now_datetime()} ], primary_action_label: __('Transfer'), primary_action : function() { @@ -147,7 +147,12 @@ let transfer_patient_dialog = function(frm) { if(dialog.get_value('service_unit')){ service_unit = dialog.get_value('service_unit'); } - if(!check_in){ + if(check_in > frappe.datetime.now_datetime()){ + frappe.msgprint({ + title: __('Not Allowed'), + message: __('Check-in time cannot be greater than the current time'), + indicator: 'red' + }); return; } frappe.call({ diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 95a836fe652..463ad6c94b4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -322,7 +322,8 @@ scheduler_events = { "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status", "erpnext.selling.doctype.quotation.quotation.set_expired_status", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status", - "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status" + "erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status", + "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", diff --git a/erpnext/hr/desk_page/hr/hr.json b/erpnext/hr/desk_page/hr/hr.json index 0fed8d322f5..895cf7290c9 100644 --- a/erpnext/hr/desk_page/hr/hr.json +++ b/erpnext/hr/desk_page/hr/hr.json @@ -78,7 +78,7 @@ "idx": 0, "is_standard": 1, "label": "HR", - "modified": "2020-06-16 19:20:50.976045", + "modified": "2020-08-11 17:04:38.655417", "modified_by": "Administrator", "module": "HR", "name": "HR", @@ -88,7 +88,7 @@ "pin_to_top": 0, "shortcuts": [ { - "color": "#9deca2", + "color": "#cef6d1", "format": "{} Active", "label": "Employee", "link_to": "Employee", @@ -96,18 +96,19 @@ "type": "DocType" }, { - "label": "Attendance", - "link_to": "Attendance", - "stats_filter": "", - "type": "DocType" - }, - { + "color": "#ffe8cd", "format": "{} Open", "label": "Leave Application", "link_to": "Leave Application", "stats_filter": "{\"status\":\"Open\"}", "type": "DocType" }, + { + "label": "Attendance", + "link_to": "Attendance", + "stats_filter": "", + "type": "DocType" + }, { "label": "Job Applicant", "link_to": "Job Applicant", diff --git a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js index ff32dbed98a..f6486743aa3 100644 --- a/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js +++ b/erpnext/manufacturing/report/downtime_analysis/downtime_analysis.js @@ -8,7 +8,7 @@ frappe.query_reports["Downtime Analysis"] = { label: __("From Date"), fieldname:"from_date", fieldtype: "Datetime", - default: frappe.datetime.add_months(frappe.datetime.now_datetime(), -1), + default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), reqd: 1 }, { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 49af0ba6ee8..67774975fd0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -718,3 +718,5 @@ erpnext.patches.v13_0.delete_report_requested_items_to_order erpnext.patches.v12_0.update_item_tax_template_company erpnext.patches.v13_0.move_branch_code_to_bank_account erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes +erpnext.patches.v13_0.stock_entry_enhancements +erpnext.patches.v12_0.update_state_code_for_daman_and_diu diff --git a/erpnext/patches/v12_0/stock_entry_enhancements.py b/erpnext/patches/v12_0/stock_entry_enhancements.py index d04b3d38622..847d92894be 100644 --- a/erpnext/patches/v12_0/stock_entry_enhancements.py +++ b/erpnext/patches/v12_0/stock_entry_enhancements.py @@ -19,7 +19,7 @@ def create_stock_entry_types(): for purpose in ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", "Material Consumption for Manufacture", "Manufacture", - "Repack", "Send to Subcontractor", "Send to Warehouse", "Receive at Warehouse"]: + "Repack", "Send to Subcontractor"]: ste_type = frappe.get_doc({ 'doctype': 'Stock Entry Type', diff --git a/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py new file mode 100644 index 00000000000..7450e9cd8c0 --- /dev/null +++ b/erpnext/patches/v12_0/update_state_code_for_daman_and_diu.py @@ -0,0 +1,22 @@ +import frappe +from erpnext.regional.india import states + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + # Update options in gst_state custom field + gst_state = frappe.get_doc('Custom Field', 'Address-gst_state') + gst_state.options = '\n'.join(states) + gst_state.save() + + # Update gst_state and state code in existing address + frappe.db.sql(""" + UPDATE `tabAddress` + SET + gst_state = 'Dadra and Nagar Haveli and Daman and Diu', + gst_state_number = 26 + WHERE gst_state = 'Daman and Diu' + """) \ No newline at end of file diff --git a/erpnext/patches/v13_0/stock_entry_enhancements.py b/erpnext/patches/v13_0/stock_entry_enhancements.py new file mode 100644 index 00000000000..dcc4f956f77 --- /dev/null +++ b/erpnext/patches/v13_0/stock_entry_enhancements.py @@ -0,0 +1,27 @@ +# Copyright(c) 2020, Frappe Technologies Pvt.Ltd.and Contributors +# License: GNU General Public License v3.See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("stock", "doctype", "stock_entry") + if frappe.db.has_column("Stock Entry", "add_to_transit"): + frappe.db.sql(""" + UPDATE `tabStock Entry` SET + stock_entry_type = 'Material Transfer', + purpose = 'Material Transfer', + add_to_transit = 1 WHERE stock_entry_type = 'Send to Warehouse' + """) + + frappe.db.sql("""UPDATE `tabStock Entry` SET + stock_entry_type = 'Material Transfer', + purpose = 'Material Transfer' + WHERE stock_entry_type = 'Receive at Warehouse' + """) + + frappe.reload_doc("stock", "doctype", "warehouse_type") + if not frappe.db.exists('Warehouse Type', 'Transit'): + doc = frappe.new_doc('Warehouse Type') + doc.name = 'Transit' + doc.insert() \ No newline at end of file diff --git a/erpnext/payroll/desk_page/payroll/payroll.json b/erpnext/payroll/desk_page/payroll/payroll.json index b5eac465c82..285e3b3a135 100644 --- a/erpnext/payroll/desk_page/payroll/payroll.json +++ b/erpnext/payroll/desk_page/payroll/payroll.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Taxation", - "links": "[\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n \n }\n]" + "links": "[\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Other Income\",\n \"name\": \"Employee Other Income\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n \n }\n]" }, { "hidden": 0, @@ -38,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Payroll", - "modified": "2020-06-19 12:23:06.034046", + "modified": "2020-08-10 19:38:45.976209", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 554484febbb..30ea432678c 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -90,7 +90,7 @@ class PayrollEntry(Document): cond = '' for f in ['company', 'branch', 'department', 'designation']: if self.get(f): - cond += " and t1." + f + " = '" + self.get(f).replace("'", "\'") + "'" + cond += " and t1." + f + " = " + frappe.db.escape(self.get(f)) return cond diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js index 5316eb45b5b..9432d421752 100644 --- a/erpnext/public/js/communication.js +++ b/erpnext/public/js/communication.js @@ -13,7 +13,7 @@ frappe.ui.form.on("Communication", { frappe.confirm(__(confirm_msg, [__("Issue")]), () => { frm.trigger('make_issue_from_communication'); }) - }, "Make"); + }, "Create"); } if(!in_list(["Lead", "Opportunity"], frm.doc.reference_doctype)) { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 405a33c72ae..69515390266 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -163,9 +163,11 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(me.frm.doc["items"] || [], function(n, item) { var item_tax_map = me._load_item_tax_rate(item.item_tax_rate); var cumulated_tax_fraction = 0.0; - + var total_inclusive_tax_amount_per_qty = 0; $.each(me.frm.doc["taxes"] || [], function(i, tax) { - tax.tax_fraction_for_current_item = me.get_current_tax_fraction(tax, item_tax_map); + var current_tax_fraction = me.get_current_tax_fraction(tax, item_tax_map); + tax.tax_fraction_for_current_item = current_tax_fraction[0]; + var inclusive_tax_amount_per_qty = current_tax_fraction[1]; if(i==0) { tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item; @@ -176,10 +178,12 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } cumulated_tax_fraction += tax.tax_fraction_for_current_item; + total_inclusive_tax_amount_per_qty += inclusive_tax_amount_per_qty * flt(item.qty); }); - if(cumulated_tax_fraction && !me.discount_amount_applied) { - item.net_amount = flt(item.amount / (1 + cumulated_tax_fraction)); + if(!me.discount_amount_applied && item.qty && (total_inclusive_tax_amount_per_qty || cumulated_tax_fraction)) { + var amount = flt(item.amount) - total_inclusive_tax_amount_per_qty; + item.net_amount = flt(amount / (1 + cumulated_tax_fraction)); item.net_rate = item.qty ? flt(item.net_amount / item.qty, precision("net_rate", item)) : 0; me.set_in_company_currency(item, ["net_rate", "net_amount"]); @@ -191,6 +195,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ // Get tax fraction for calculating tax exclusive amount // from tax inclusive amount var current_tax_fraction = 0.0; + var inclusive_tax_amount_per_qty = 0; if(cint(tax.included_in_print_rate)) { var tax_rate = this._get_tax_rate(tax, item_tax_map); @@ -205,13 +210,16 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(tax.charge_type == "On Previous Row Total") { current_tax_fraction = (tax_rate / 100.0) * this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_fraction_for_current_item; + } else if (tax.charge_type == "On Item Quantity") { + inclusive_tax_amount_per_qty = flt(tax_rate); } } - if(tax.add_deduct_tax) { - current_tax_fraction *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; + if(tax.add_deduct_tax && tax.add_deduct_tax == "Deduct") { + current_tax_fraction *= -1; + inclusive_tax_amount_per_qty *= -1; } - return current_tax_fraction; + return [current_tax_fraction, inclusive_tax_amount_per_qty]; }, _get_tax_rate: function(tax, item_tax_map) { @@ -360,8 +368,9 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ } else if(tax.charge_type == "On Previous Row Total") { current_tax_amount = (tax_rate / 100.0) * this.frm.doc["taxes"][cint(tax.row_id) - 1].grand_total_for_current_item; + } else if (tax.charge_type == "On Item Quantity") { + current_tax_amount = tax_rate * item.qty; } - this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount); return current_tax_amount; @@ -573,7 +582,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ var actual_taxes_dict = {}; $.each(this.frm.doc["taxes"] || [], function(i, tax) { - if (tax.charge_type == "Actual") { + if (in_list(["Actual", "On Item Quantity"], tax.charge_type)) { var tax_amount = (tax.category == "Valuation") ? 0.0 : tax.tax_amount; tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0; actual_taxes_dict[tax.idx] = tax_amount; @@ -586,7 +595,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 4e50f3d7f67..436a232a551 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1821,7 +1821,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }, set_query_for_item_tax_template: function(doc, cdt, cdn) { - var item = frappe.get_doc(cdt, cdn); if(!item.item_code) { frappe.throw(__("Please enter Item Code to get item taxes")); @@ -1829,7 +1828,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ let filters = { 'item_code': item.item_code, - 'valid_from': doc.transaction_date || doc.bill_date || doc.posting_date, + 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'item_group': item.item_group, } diff --git a/erpnext/public/less/website.less b/erpnext/public/less/website.less index 57a0a332a98..ac878de105b 100644 --- a/erpnext/public/less/website.less +++ b/erpnext/public/less/website.less @@ -297,6 +297,10 @@ margin-top: 30px; } +.item-group-slideshow { + margin-bottom: 1rem; +} + .product-image-img { border: 1px solid @light-border-color; border-radius: 3px; diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 0ed98b74eef..d6221a80aa2 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -10,8 +10,7 @@ states = [ 'Bihar', 'Chandigarh', 'Chhattisgarh', - 'Dadra and Nagar Haveli', - 'Daman and Diu', + 'Dadra and Nagar Haveli and Daman and Diu', 'Delhi', 'Goa', 'Gujarat', @@ -50,8 +49,7 @@ state_numbers = { "Bihar": "10", "Chandigarh": "04", "Chhattisgarh": "22", - "Dadra and Nagar Haveli": "26", - "Daman and Diu": "25", + "Dadra and Nagar Haveli and Daman and Diu": "26", "Delhi": "07", "Goa": "30", "Gujarat": "24", diff --git a/erpnext/regional/india/gst_state_code_data.json b/erpnext/regional/india/gst_state_code_data.json index 6dab81d6688..ff88e0f9d6c 100644 --- a/erpnext/regional/india/gst_state_code_data.json +++ b/erpnext/regional/india/gst_state_code_data.json @@ -134,15 +134,10 @@ "state_code": "DL", "state_name": "Delhi" }, - { - "state_number": "25", - "state_code": "DD", - "state_name": "Daman and Diu" - }, { "state_number": "26", "state_code": "DN", - "state_name": "Dadra and Nagar Haveli" + "state_name": "Dadra and Nagar Haveli and Daman and Diu" }, { "state_number": "22", diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 8885b88c2a4..282efe47901 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -131,6 +131,9 @@ class Gstr1Report(object): taxable_value += abs(net_amount) elif tax_rate: taxable_value += abs(net_amount) + elif not tax_rate and self.filters.get('type_of_business') == 'EXPORT' \ + and invoice_details.get('export_type') == "Without Payment of Tax": + taxable_value += abs(net_amount) row += [tax_rate or 0, taxable_value] diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 22e30e31139..09e474dc2ef 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -12,7 +12,8 @@ def get_data(): 'Payment Entry': 'party', 'Quotation': 'party_name', 'Opportunity': 'party_name', - 'Bank Account': 'party' + 'Bank Account': 'party', + 'Subscription': 'party' }, 'dynamic_links': { 'party_name': ['Customer', 'quotation_to'] diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 483ef78d64c..ae5471b9000 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -35,7 +35,8 @@ erpnext.PointOfSale.Controller = class { create_opening_voucher() { const table_fields = [ { fieldname: "mode_of_payment", fieldtype: "Link", in_list_view: 1, label: "Mode of Payment", options: "Mode of Payment", reqd: 1 }, - { fieldname: "opening_amount", fieldtype: "Currency", in_list_view: 1, label: "Opening Amount", options: "company:company_currency", reqd: 1 } + { fieldname: "opening_amount", fieldtype: "Currency", default: 0, in_list_view: 1, label: "Opening Amount", + options: "company:company_currency", reqd: 1 } ]; const dialog = new frappe.ui.Dialog({ @@ -66,7 +67,7 @@ erpnext.PointOfSale.Controller = class { frappe.db.get_doc("POS Closing Entry", pos_closing_entry.name).then(({ payment_reconciliation }) => { dialog.fields_dict.balance_details.df.data = []; payment_reconciliation.forEach(pay => { - const { mode_of_payment, closing_amount } = pay; + const { mode_of_payment } = pay; dialog.fields_dict.balance_details.df.data.push({ mode_of_payment: mode_of_payment }); @@ -152,7 +153,7 @@ erpnext.PointOfSale.Controller = class { }, () => this.make_new_invoice(), () => frappe.dom.unfreeze(), - () => this.page.set_title(__('Point of Sale Beta')), + () => this.page.set_title(__('Point of Sale')), ]); } diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 7ae5385a23c..f882db60c5d 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -34,6 +34,16 @@ frappe.ui.form.on("Company", { frm.set_query("default_buying_terms", function() { return { filters: { buying: 1 } }; }); + + frm.set_query("default_in_transit_warehouse", function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); }, company_name: function(frm) { diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 221044df3a7..4a26a71970c 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -25,6 +25,7 @@ "default_selling_terms", "default_buying_terms", "default_warehouse_for_sales_return", + "default_in_transit_warehouse", "column_break_10", "country", "create_chart_of_accounts_based_on", @@ -242,7 +243,7 @@ { "fieldname": "default_warehouse_for_sales_return", "fieldtype": "Link", - "label": "Default warehouse for Sales Return", + "label": "Default Warehouse for Sales Return", "options": "Warehouse" }, { @@ -733,6 +734,12 @@ "fieldname": "enable_perpetual_inventory_for_non_stock_items", "fieldtype": "Check", "label": "Enable Perpetual Inventory For Non Stock Items" + }, + { + "fieldname": "default_in_transit_warehouse", + "fieldtype": "Link", + "label": "Default In Transit Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-building", @@ -740,7 +747,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-06-24 12:45:31.462195", + "modified": "2020-08-06 00:38:08.311216", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -801,4 +808,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 47b41a97ad3..8e707fe3f49 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -140,7 +140,8 @@ class Company(NestedSet): {"warehouse_name": _("All Warehouses"), "is_group": 1}, {"warehouse_name": _("Stores"), "is_group": 0}, {"warehouse_name": _("Work In Progress"), "is_group": 0}, - {"warehouse_name": _("Finished Goods"), "is_group": 0}]: + {"warehouse_name": _("Finished Goods"), "is_group": 0}, + {"warehouse_name": _("Goods In Transit"), "is_group": 0, "warehouse_type": "Transit"}]: if not frappe.db.exists("Warehouse", "{0} - {1}".format(wh_detail["warehouse_name"], self.abbr)): warehouse = frappe.get_doc({ @@ -149,7 +150,8 @@ class Company(NestedSet): "is_group": wh_detail["is_group"], "company": self.name, "parent_warehouse": "{0} - {1}".format(_("All Warehouses"), self.abbr) \ - if not wh_detail["is_group"] else "" + if not wh_detail["is_group"] else "", + "warehouse_type" : wh_detail["warehouse_type"] if "warehouse_type" in wh_detail else None }) warehouse.flags.ignore_permissions = True warehouse.flags.ignore_mandatory = True diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index ad063cfc9d1..72ed00293ed 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -95,8 +95,6 @@ def install(country=None): {'doctype': 'Stock Entry Type', 'name': 'Send to Subcontractor', 'purpose': 'Send to Subcontractor'}, {'doctype': 'Stock Entry Type', 'name': 'Material Transfer for Manufacture', 'purpose': 'Material Transfer for Manufacture'}, {'doctype': 'Stock Entry Type', 'name': 'Material Consumption for Manufacture', 'purpose': 'Material Consumption for Manufacture'}, - {'doctype': 'Stock Entry Type', 'name': 'Send to Warehouse', 'purpose': 'Send to Warehouse'}, - {'doctype': 'Stock Entry Type', 'name': 'Receive at Warehouse', 'purpose': 'Receive at Warehouse'}, # Designation {'doctype': 'Designation', 'designation_name': _('CEO')}, @@ -244,7 +242,10 @@ def install(country=None): {"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")}, {"doctype": "Sales Stage", "stage_name": _("Perception Analysis")}, {"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")}, - {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")} + {"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")}, + + # Warehouse Type + {'doctype': 'Warehouse Type', 'name': 'Transit'}, ] from erpnext.setup.setup_wizard.data.industry_type import get_industry_types diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json index 66efcf8cd85..ea385c8b2a9 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.json +++ b/erpnext/stock/doctype/delivery_note/delivery_note.json @@ -175,7 +175,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "MAT-DN-.YYYY.-", + "options": "MAT-DN-.YYYY.-\nMAT-DN-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -1255,7 +1255,7 @@ "idx": 146, "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:13:55.580420", + "modified": "2020-08-03 23:18:47.739997", "modified_by": "Administrator", "module": "Stock", "name": "Delivery Note", diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 45526a3910e..d07b3dc4fef 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -123,6 +123,7 @@ "weightage", "slideshow", "website_image", + "website_image_alt", "thumbnail", "cb72", "website_warehouse", @@ -1054,15 +1055,21 @@ "fieldtype": "Data", "label": "Default Manufacturer Part No", "read_only": 1 + }, + { + "fieldname": "website_image_alt", + "fieldtype": "Data", + "label": "Image Description" } ], "has_web_view": 1, "icon": "fa fa-tag", "idx": 2, "image_field": "image", + "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2020-07-31 21:21:10.956453", + "modified": "2020-08-07 14:24:58.384992", "modified_by": "Administrator", "module": "Stock", "name": "Item", @@ -1124,4 +1131,4 @@ "sort_order": "DESC", "title_field": "item_name", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index d7b43bf399b..d209f483536 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -343,7 +343,7 @@ class Item(WebsiteGenerator): if variant: context.variant = frappe.get_doc("Item", variant) - for fieldname in ("website_image", "web_long_description", "description", + for fieldname in ("website_image", "website_image_alt", "web_long_description", "description", "website_specifications"): if context.variant.get(fieldname): value = context.variant.get(fieldname) diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json index d1f29e364a6..44503d22a3c 100644 --- a/erpnext/stock/doctype/material_request/material_request.json +++ b/erpnext/stock/doctype/material_request/material_request.json @@ -11,6 +11,7 @@ "naming_series", "title", "material_request_type", + "transfer_status", "customer", "column_break_2", "schedule_date", @@ -303,13 +304,22 @@ "fieldtype": "Link", "label": "Set From Warehouse", "options": "Warehouse" + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.add_to_transit == 1", + "fieldname": "transfer_status", + "fieldtype": "Select", + "label": "Transfer Status", + "options": "\nNot Started\nIn Transit\nCompleted", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 70, "is_submittable": 1, "links": [], - "modified": "2020-05-01 20:21:09.990867", + "modified": "2020-08-10 13:27:54.891058", "modified_by": "Administrator", "module": "Stock", "name": "Material Request", diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js index 614ecb8a8f9..0d7095875c6 100644 --- a/erpnext/stock/doctype/material_request/material_request_list.js +++ b/erpnext/stock/doctype/material_request/material_request_list.js @@ -1,8 +1,16 @@ frappe.listview_settings['Material Request'] = { - add_fields: ["material_request_type", "status", "per_ordered", "per_received"], + add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"], get_indicator: function(doc) { if(doc.status=="Stopped") { return [__("Stopped"), "red", "status,=,Stopped"]; + } else if(doc.transfer_status && doc.docstatus != 2) { + if (doc.transfer_status == "Not Started") { + return [__("Not Started"), "orange"]; + } else if (doc.transfer_status == "In Transit") { + return [__("In Transit"), "yellow"]; + } else if (doc.transfer_status == "Completed") { + return [__("Completed"), "green"]; + } } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) { return [__("Pending"), "orange", "per_ordered,=,0"]; } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) { diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json index 92e33ca64e3..ce54fc883f6 100755 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json @@ -1,7 +1,6 @@ { "actions": [], "allow_import": 1, - "allow_workflow": 1, "autoname": "naming_series:", "creation": "2013-05-21 16:16:39", "doctype": "DocType", @@ -160,7 +159,7 @@ "no_copy": 1, "oldfieldname": "naming_series", "oldfieldtype": "Select", - "options": "MAT-PRE-.YYYY.-", + "options": "MAT-PRE-.YYYY.-\nMAT-PR-RET-.YYYY.-", "print_hide": 1, "reqd": 1, "set_only_once": 1 @@ -1110,7 +1109,7 @@ "idx": 261, "is_submittable": 1, "links": [], - "modified": "2020-07-18 05:19:12.148115", + "modified": "2020-08-03 23:20:26.381024", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 53b986cb72c..9845bc2f705 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -19,7 +19,6 @@ frappe.ui.form.on('Stock Entry', { filters: [ ['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'per_transferred', '<','100'], - ['Stock Entry', 'purpose', '=', 'Send to Warehouse'] ] } }); @@ -171,9 +170,9 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus === 1 && frm.doc.purpose == 'Send to Warehouse') { - if (frm.doc.per_transferred < 100) { - frm.add_custom_button(__('Receive at Warehouse Entry'), function() { + if (frm.doc.docstatus === 1) { + if (frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer' && frm.doc.per_transferred < 100) { + frm.add_custom_button('End Transit', function() { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.stock_entry.stock_entry.make_stock_in_entry", frm: frm @@ -266,6 +265,7 @@ frappe.ui.form.on('Stock Entry', { stock_entry_type: function(frm){ frm.remove_custom_button('Bill of Materials', "Get items from"); frm.events.show_bom_custom_button(frm); + frm.trigger('add_to_transit'); }, purpose: function(frm) { @@ -532,6 +532,26 @@ frappe.ui.form.on('Stock Entry', { target_warehouse_address: function(frm) { erpnext.utils.get_address_display(frm, 'target_warehouse_address', 'target_address_display', false); + }, + + add_to_transit: function(frm) { + if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { + frm.set_value('stock_entry_type', 'Material Transfer'); + frm.fields_dict.to_warehouse.get_query = function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }; + frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => { + if (r.default_in_transit_warehouse) { + frm.set_value('to_warehouse', r.default_in_transit_warehouse); + } + }); + } } }) @@ -754,6 +774,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ } erpnext.hide_company(); erpnext.utils.add_item(this.frm); + this.frm.trigger('add_to_transit'); }, scan_barcode: function() { @@ -919,8 +940,6 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ doc.purpose!='Material Issue'); this.frm.fields_dict["items"].grid.set_column_disp("additional_cost", doc.purpose!='Material Issue'); - this.frm.toggle_reqd("outgoing_stock_entry", - doc.purpose == 'Receive at Warehouse' ? 1: 0); }, supplier: function(doc) { diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index 704ae41bc5f..61e0df67238 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -13,6 +13,7 @@ "stock_entry_type", "outgoing_stock_entry", "purpose", + "add_to_transit", "work_order", "purchase_order", "delivery_note_no", @@ -116,11 +117,12 @@ "reqd": 1 }, { - "depends_on": "eval:doc.purpose == 'Receive at Warehouse'", + "depends_on": "eval:doc.purpose == 'Material Transfer'", "fieldname": "outgoing_stock_entry", "fieldtype": "Link", "label": "Stock Entry (Outward GIT)", - "options": "Stock Entry" + "options": "Stock Entry", + "read_only": 1 }, { "bold": 1, @@ -132,7 +134,7 @@ "label": "Purpose", "oldfieldname": "purpose", "oldfieldtype": "Select", - "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nSend to Warehouse\nReceive at Warehouse", + "options": "Material Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", "read_only": 1 }, { @@ -630,13 +632,21 @@ { "fieldname": "print_settings_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval: doc.purpose=='Material Transfer' && !doc.outgoing_stock_entry", + "fieldname": "add_to_transit", + "fieldtype": "Check", + "label": "Add to Transit", + "no_copy": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-04-23 12:56:52.881752", + "modified": "2020-08-11 19:10:07.954981", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 229cf027bd5..30bcccdda65 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -96,6 +96,11 @@ class StockEntry(StockController): self.update_quality_inspection() if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() + + if self.purpose == 'Material Transfer' and self.add_to_transit: + self.set_material_request_transfer_status('In Transit') + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + self.set_material_request_transfer_status('Completed') def on_cancel(self): @@ -116,6 +121,11 @@ class StockEntry(StockController): self.update_quality_inspection() self.delete_auto_created_batches() + if self.purpose == 'Material Transfer' and self.add_to_transit: + self.set_material_request_transfer_status('Not Started') + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + self.set_material_request_transfer_status('In Transit') + def set_job_card_data(self): if self.job_card and not self.work_order: data = frappe.db.get_value('Job Card', @@ -133,7 +143,7 @@ class StockEntry(StockController): def validate_purpose(self): valid_purposes = ["Material Issue", "Material Receipt", "Material Transfer", "Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor", - "Material Consumption for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Consumption for Manufacture"] if self.purpose not in valid_purposes: frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes))) @@ -199,7 +209,8 @@ class StockEntry(StockController): item.set(f, item_details.get(f)) if not item.transfer_qty and item.qty: - item.transfer_qty = item.qty * item.conversion_factor + item.transfer_qty = flt(flt(item.qty) * flt(item.conversion_factor), + self.precision("transfer_qty", item)) if (self.purpose in ("Material Transfer", "Material Transfer for Manufacture") and not item.serial_no @@ -258,10 +269,10 @@ class StockEntry(StockController): """perform various (sometimes conditional) validations on warehouse""" source_mandatory = ["Material Issue", "Material Transfer", "Send to Subcontractor", "Material Transfer for Manufacture", - "Material Consumption for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Consumption for Manufacture"] target_mandatory = ["Material Receipt", "Material Transfer", "Send to Subcontractor", - "Material Transfer for Manufacture", "Send to Warehouse", "Receive at Warehouse"] + "Material Transfer for Manufacture"] validate_for_manufacture = any([d.bom_no for d in self.get("items")]) @@ -809,7 +820,7 @@ class StockEntry(StockController): def set_items_for_stock_in(self): self.items = [] - if self.outgoing_stock_entry and self.purpose == 'Receive at Warehouse': + if self.outgoing_stock_entry and self.purpose == 'Material Transfer': doc = frappe.get_doc('Stock Entry', self.outgoing_stock_entry) if doc.per_transferred == 100: @@ -1210,13 +1221,25 @@ class StockEntry(StockController): def validate_with_material_request(self): for item in self.get("items"): - if item.material_request: + material_request = item.material_request or None + material_request_item = item.material_request_item or None + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: + parent_se = frappe.get_value("Stock Entry Detail", item.ste_detail, ['material_request','material_request_item'],as_dict=True) + if parent_se: + material_request = parent_se.material_request + material_request_item = parent_se.material_request_item + + if material_request: mreq_item = frappe.db.get_value("Material Request Item", - {"name": item.material_request_item, "parent": item.material_request}, + {"name": material_request_item, "parent": material_request}, ["item_code", "warehouse", "idx"], as_dict=True) - if mreq_item.item_code != item.item_code or \ - mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse): - frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx), + if mreq_item.item_code != item.item_code: + frappe.throw(_("Item for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError) + elif self.purpose == "Material Transfer" and self.add_to_transit: + continue + elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse): + frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx), frappe.MappingMismatchError) def validate_batch(self): @@ -1284,7 +1307,7 @@ class StockEntry(StockController): to fullfill Sales Order {2}.").format(item.item_code, sr, sales_order)) def update_transferred_qty(self): - if self.purpose == 'Receive at Warehouse': + if self.purpose == 'Material Transfer' and self.outgoing_stock_entry: stock_entries = {} stock_entries_child_list = [] for d in self.items: @@ -1342,6 +1365,20 @@ class StockEntry(StockController): 'reference_type': reference_type, 'reference_name': reference_name }) + def set_material_request_transfer_status(self, status): + material_requests = [] + if self.outgoing_stock_entry: + parent_se = frappe.get_value("Stock Entry", self.outgoing_stock_entry, 'add_to_transit') + + for item in self.items: + material_request = item.material_request or None + if self.purpose == "Material Transfer" and material_request not in material_requests: + if self.outgoing_stock_entry and parent_se: + material_request = frappe.get_value("Stock Entry Detail", item.ste_detail, 'material_request') + + if material_request and material_request not in material_requests: + material_requests.append(material_request) + frappe.db.set_value('Material Request', material_request, 'transfer_status', status) @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): @@ -1381,12 +1418,19 @@ def move_sample_to_retention_warehouse(company, items): @frappe.whitelist() def make_stock_in_entry(source_name, target_doc=None): + def set_missing_values(source, target): - target.purpose = 'Receive at Warehouse' target.set_stock_entry_type() def update_item(source_doc, target_doc, source_parent): target_doc.t_warehouse = '' + + if source_doc.material_request_item and source_doc.material_request : + add_to_transit = frappe.db.get_value('Stock Entry', source_name, 'add_to_transit') + if add_to_transit: + warehouse = frappe.get_value('Material Request Item', source_doc.material_request_item, 'warehouse') + target_doc.t_warehouse = warehouse + target_doc.s_warehouse = source_doc.t_warehouse target_doc.qty = source_doc.qty - source_doc.transferred_qty diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index 8e25804511e..d98870de3eb 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -737,34 +737,6 @@ class TestStockEntry(unittest.TestCase): self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1) self.assertEqual(se.get("items")[0].amount, 0) - def test_goods_in_transit(self): - from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse - warehouse = "_Test Warehouse FG 1 - _TC" - - if not frappe.db.exists('Warehouse', warehouse): - create_warehouse("_Test Warehouse FG 1") - - outward_entry = make_stock_entry(item_code="_Test Item", - purpose="Send to Warehouse", - source="_Test Warehouse - _TC", - target="_Test Warehouse 1 - _TC", qty=50, basic_rate=100) - - inward_entry1 = make_stock_in_entry(outward_entry.name) - inward_entry1.items[0].t_warehouse = warehouse - inward_entry1.items[0].qty = 25 - inward_entry1.submit() - - doc = frappe.get_doc('Stock Entry', outward_entry.name) - self.assertEqual(doc.per_transferred, 50) - - inward_entry2 = make_stock_in_entry(outward_entry.name) - inward_entry2.items[0].t_warehouse = warehouse - inward_entry2.items[0].qty = 25 - inward_entry2.submit() - - doc = frappe.get_doc('Stock Entry', outward_entry.name) - self.assertEqual(doc.per_transferred, 100) - def test_gle_for_opening_stock_entry(self): mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True) diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json index edee3c7dc9a..0f2b55ec342 100644 --- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json +++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json @@ -1,156 +1,83 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "Prompt", - "beta": 0, "creation": "2019-03-13 16:23:46.636769", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "purpose" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "Material Issue", - "fetch_if_empty": 0, "fieldname": "purpose", "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Purpose", - "length": 0, - "no_copy": 0, - "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor\nSend to Warehouse\nReceive at Warehouse", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, + "options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor", "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 + "set_only_once": 1 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-26 12:02:42.144377", + "links": [], + "modified": "2020-08-10 23:24:37.160817", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Type", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Manufacturing Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Stock Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "Stock User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html index 0dd4c3505e1..5d46a45053d 100644 --- a/erpnext/templates/generators/item/item_image.html +++ b/erpnext/templates/generators/item/item_image.html @@ -23,7 +23,7 @@ }) {% else %} -{{ product_image(website_image or image or 'no-image.jpg') }} +{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }} {% endif %} diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index 3f984536037..40a064fc768 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -4,7 +4,7 @@ {% block page_content %}
-
+
{% if slideshow %} {% include "templates/includes/slideshow.html" %} {% endif %} diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html index 3c82e90cc0d..ea6b00fc58a 100644 --- a/erpnext/templates/includes/macros.html +++ b/erpnext/templates/includes/macros.html @@ -7,9 +7,9 @@
{% endmacro %} -{% macro product_image(website_image, css_class="") %} +{% macro product_image(website_image, css_class="", alt="") %}
- + {{ alt }}
{% endmacro %}