diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json
index 85c9209205..b2a3f83e5f 100644
--- a/erpnext/accounts/desk_page/accounting/accounting.json
+++ b/erpnext/accounts/desk_page/accounting/accounting.json
@@ -79,6 +79,11 @@
"hidden": 0,
"label": "Profitability",
"links": "[\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Gross Profit\",\n \"name\": \"Gross Profit\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profitability Analysis\",\n \"name\": \"Profitability Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"type\": \"report\"\n }\n]"
+ },
+ {
+ "hidden": 0,
+ "label": "Value-Added Tax (VAT UAE)",
+ "links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n\n]"
}
],
"category": "Modules",
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 4573c50134..b7bbb74ce9 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-03-25 10:53:52",
@@ -503,7 +504,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2020-06-02 18:15:46.955697",
+ "modified": "2020-10-30 13:56:01.121995",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 72149a665d..2e1f201e25 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2016-06-01 14:38:51.012597",
@@ -587,7 +588,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-02 13:39:43.383705",
+ "modified": "2020-10-30 13:56:20.007336",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 1cff3c661d..5bc57b4a84 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2020-01-24 15:29:29.933693",
@@ -1580,7 +1581,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2020-09-28 16:51:24.641755",
+ "modified": "2020-10-30 13:56:51.056083",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index a7e20a0c32..d486ff6028 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -39,6 +39,7 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items_qty()
+ self.validate_non_stock_items()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
@@ -174,6 +175,14 @@ class POSInvoice(SalesInvoice):
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against)
)
+
+ def validate_non_stock_items(self):
+ for d in self.get("items"):
+ is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
+ if not is_stock_item:
+ frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice. ").format(
+ d.idx, frappe.bold(d.item_code)
+ ), title=_("Invalid Item"))
def validate_mode_of_payment(self):
if len(self.payments) == 0:
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 8925b87b52..2df77a84c7 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -1334,7 +1335,8 @@
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
- "modified": "2020-09-21 12:22:09.164068",
+ "links": [],
+ "modified": "2020-10-30 13:57:18.266978",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
@@ -1396,4 +1398,4 @@
"timeline_field": "supplier",
"title_field": "title",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 26acad3a4f..fb30153bf6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1003,7 +1003,7 @@ def make_purchase_invoice(**args):
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"conversion_factor": 1.0,
"serial_no": args.serial_no,
- "stock_uom": "_Test UOM",
+ "stock_uom": args.uom or "_Test UOM",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"project": args.project,
"rejected_warehouse": args.rejected_warehouse or "",
@@ -1045,7 +1045,8 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No"
- pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
+ if args.supplier_warehouse:
+ pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index ae40153890..17fbe2def9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:05",
@@ -1955,7 +1956,7 @@
"idx": 181,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-09 15:59:57.544736",
+ "modified": "2020-10-30 13:57:45.086303",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 4b865a98e9..71231f68c8 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -1105,7 +1106,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 11:39:37.388249",
+ "modified": "2020-10-30 13:58:14.697921",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 9a092ca5c3..b39c989073 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:45",
@@ -807,7 +808,7 @@
"idx": 29,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-01 20:56:17.932007",
+ "modified": "2020-10-30 13:58:33.043971",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 78ef66585f..b4c57d7c91 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -250,7 +250,11 @@ doc_events = {
"on_trash": "erpnext.regional.check_deletion_permission"
},
"Purchase Invoice": {
- "validate": "erpnext.regional.india.utils.update_grand_total_for_rcm"
+ "validate": [
+ "erpnext.regional.india.utils.update_grand_total_for_rcm",
+ "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
+ "erpnext.regional.united_arab_emirates.utils.validate_returns"
+ ]
},
"Payment Entry": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
@@ -391,7 +395,8 @@ regional_overrides = {
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries'
},
'United Arab Emirates': {
- 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'
+ 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
+ 'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.united_arab_emirates.utils.make_regional_gl_entries',
},
'Saudi Arabia': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data'
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 8913c648c5..c1dcc97b1a 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -32,7 +32,7 @@ class LeaveEncashment(Document):
additional_salary.employee = self.employee
earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component")
if not earning_component:
- frappe.throw(_("Please set Earning Component for Leave type: {0}.".format(self.leave_type)))
+ frappe.throw(_("Please set Earning Component for Leave type: {0}.").format(self.leave_type))
additional_salary.salary_component = earning_component
additional_salary.payroll_date = self.encashment_date
additional_salary.amount = self.encashment_amount
@@ -98,7 +98,11 @@ class LeaveEncashment(Document):
create_leave_ledger_entry(self, args, submit)
# create reverse entry for expired leaves
- to_date = self.get_leave_allocation().get('to_date')
+ leave_allocation = self.get_leave_allocation()
+ if not leave_allocation:
+ return
+
+ to_date = leave_allocation.get('to_date')
if to_date < getdate(nowdate()):
args = frappe._dict(
leaves=self.encashable_days,
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index bac6e638d7..e59db4c12d 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -127,6 +127,7 @@ def create_loan(source_name, target_doc=None, submit=0):
target_doc.loan_account = account_details.loan_account
target_doc.interest_income_account = account_details.interest_income_account
target_doc.penalty_income_account = account_details.penalty_income_account
+ target_doc.loan_application = source_name
doclist = get_mapped_doc("Loan Application", source_name, {
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index 4c85cb60e8..7d15abaa3b 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -70,7 +70,7 @@ class Membership(Document):
settings = frappe.get_doc("Membership Settings")
if not member.customer:
- frappe.throw(_("No customer linked to member {}", [member.name]))
+ frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
if not settings.debit_account:
frappe.throw(_("You need to set Debit Account in Membership Settings"))
diff --git a/erpnext/patches/v12_0/generate_leave_ledger_entries.py b/erpnext/patches/v12_0/generate_leave_ledger_entries.py
index 7afde373c3..fe072d7eb9 100644
--- a/erpnext/patches/v12_0/generate_leave_ledger_entries.py
+++ b/erpnext/patches/v12_0/generate_leave_ledger_entries.py
@@ -51,7 +51,7 @@ def generate_encashment_leave_ledger_entries():
for encashment in leave_encashments:
if not frappe.db.exists("Leave Ledger Entry", {'transaction_type': 'Leave Encashment', 'transaction_name': encashment.name}):
- frappe.get_doc("Leave Enchashment", encashment).create_leave_ledger_entry()
+ frappe.get_doc("Leave Encashment", encashment).create_leave_ledger_entry()
def generate_expiry_allocation_ledger_entries():
''' fix ledger entries for missing leave allocation transaction '''
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 30ea432678..a3d12c35c0 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -344,9 +344,13 @@ class PayrollEntry(Document):
employees_to_mark_attendance = []
days_in_payroll, days_holiday, days_attendance_marked = 0, 0, 0
for employee_detail in self.employees:
- days_holiday = self.get_count_holidays_of_employee(employee_detail.employee)
- days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee)
- days_in_payroll = date_diff(self.end_date, self.start_date) + 1
+ employee_joining_date = frappe.db.get_value("Employee", employee_detail.employee, 'date_of_joining')
+ start_date = self.start_date
+ if employee_joining_date > getdate(self.start_date):
+ start_date = employee_joining_date
+ days_holiday = self.get_count_holidays_of_employee(employee_detail.employee, start_date)
+ days_attendance_marked = self.get_count_employee_attendance(employee_detail.employee, start_date)
+ days_in_payroll = date_diff(self.end_date, start_date) + 1
if days_in_payroll > days_holiday + days_attendance_marked:
employees_to_mark_attendance.append({
"employee": employee_detail.employee,
@@ -354,22 +358,25 @@ class PayrollEntry(Document):
})
return employees_to_mark_attendance
- def get_count_holidays_of_employee(self, employee):
+ def get_count_holidays_of_employee(self, employee, start_date):
holiday_list = get_holiday_list_for_employee(employee)
holidays = 0
if holiday_list:
days = frappe.db.sql("""select count(*) from tabHoliday where
parent=%s and holiday_date between %s and %s""", (holiday_list,
- self.start_date, self.end_date))
+ start_date, self.end_date))
if days and days[0][0]:
holidays = days[0][0]
return holidays
- def get_count_employee_attendance(self, employee):
+ def get_count_employee_attendance(self, employee, start_date):
marked_days = 0
- attendances = frappe.db.sql("""select count(*) from tabAttendance where
- employee=%s and docstatus=1 and attendance_date between %s and %s""",
- (employee, self.start_date, self.end_date))
+ attendances = frappe.get_all("Attendance",
+ fields = ["count(*)"],
+ filters = {
+ "employee": employee,
+ "attendance_date": ('between', [start_date, self.end_date])
+ }, as_list=1)
if attendances and attendances[0][0]:
marked_days = attendances[0][0]
return marked_days
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 4ccf56435d..cecb8cde7c 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -240,7 +240,6 @@ class SalarySlip(TransactionBase):
self.absent_days += unmarked_days #will be treated as absent
self.payment_days -= unmarked_days
if include_holidays_in_total_working_days:
- self.absent_days -= len(holidays)
for holiday in holidays:
if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
self.payment_days += 1
diff --git a/erpnext/payroll/report/bank_remittance/bank_remittance.py b/erpnext/payroll/report/bank_remittance/bank_remittance.py
index 4b052bf5c4..500543ceb0 100644
--- a/erpnext/payroll/report/bank_remittance/bank_remittance.py
+++ b/erpnext/payroll/report/bank_remittance/bank_remittance.py
@@ -47,33 +47,39 @@ def execute(filters=None):
"fieldtype": "Int",
"fieldname": "employee_account_no",
"width": 50
- },
- {
+ }
+ ]
+
+ if frappe.db.has_column('Employee', 'ifsc_code'):
+ columns.append({
"label": _("IFSC Code"),
"fieldtype": "Data",
"fieldname": "bank_code",
"width": 100
- },
- {
- "label": _("Currency"),
- "fieldtype": "Data",
- "fieldname": "currency",
- "width": 50
- },
- {
- "label": _("Net Salary Amount"),
- "fieldtype": "Currency",
- "options": "currency",
- "fieldname": "amount",
- "width": 100
- }
- ]
+ })
+
+ columns += [{
+ "label": _("Currency"),
+ "fieldtype": "Data",
+ "fieldname": "currency",
+ "width": 50
+ },
+ {
+ "label": _("Net Salary Amount"),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "fieldname": "amount",
+ "width": 100
+ }]
+
data = []
accounts = get_bank_accounts()
payroll_entries = get_payroll_entries(accounts, filters)
salary_slips = get_salary_slips(payroll_entries)
- get_emp_bank_ifsc_code(salary_slips)
+
+ if frappe.db.has_column('Employee', 'ifsc_code'):
+ get_emp_bank_ifsc_code(salary_slips)
for salary in salary_slips:
if salary.bank_name and salary.bank_account_no and salary.debit_acc_no and salary.status in ["Submitted", "Paid"]:
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index 6e97d811fc..29f35958e1 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -146,18 +146,18 @@ cur_frm.cscript.account_head = function(doc, cdt, cdn) {
if(!d.charge_type && d.account_head){
frappe.msgprint(__("Please select Charge Type first"));
frappe.model.set_value(cdt, cdn, "account_head", "");
- } else if(d.account_head && d.charge_type!=="Actual") {
+ } else if (d.account_head) {
frappe.call({
type:"GET",
method: "erpnext.controllers.accounts_controller.get_tax_rate",
args: {"account_head":d.account_head},
callback: function(r) {
- frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0);
+ if (d.charge_type!=="Actual") {
+ frappe.model.set_value(cdt, cdn, "rate", r.message.tax_rate || 0);
+ }
frappe.model.set_value(cdt, cdn, "description", r.message.account_name);
}
})
- } else if (d.charge_type == 'Actual' && d.account_head) {
- frappe.model.set_value(cdt, cdn, "description", d.account_head.split(' - ')[0]);
}
}
diff --git a/erpnext/regional/doctype/uae_vat_account/__init__.py b/erpnext/regional/doctype/uae_vat_account/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/regional/doctype/uae_vat_account/uae_vat_account.json b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.json
new file mode 100644
index 0000000000..73a8169207
--- /dev/null
+++ b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.json
@@ -0,0 +1,35 @@
+{
+ "actions": [],
+ "autoname": "account",
+ "creation": "2020-09-28 11:30:45.472053",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "account"
+ ],
+ "fields": [
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_preview": 1,
+ "label": "Account",
+ "options": "Account"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-09-28 12:02:56.444007",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "UAE VAT Account",
+ "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/regional/doctype/uae_vat_account/uae_vat_account.py b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.py
new file mode 100644
index 0000000000..80d6b3a5f1
--- /dev/null
+++ b/erpnext/regional/doctype/uae_vat_account/uae_vat_account.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 UAEVATAccount(Document):
+ pass
diff --git a/erpnext/regional/doctype/uae_vat_settings/__init__.py b/erpnext/regional/doctype/uae_vat_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/regional/doctype/uae_vat_settings/test_uae_vat_settings.py b/erpnext/regional/doctype/uae_vat_settings/test_uae_vat_settings.py
new file mode 100644
index 0000000000..b88439f9b8
--- /dev/null
+++ b/erpnext/regional/doctype/uae_vat_settings/test_uae_vat_settings.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 TestUAEVATSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js
new file mode 100644
index 0000000000..07a93010b5
--- /dev/null
+++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('UAE VAT Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json
new file mode 100644
index 0000000000..ce2c1d4e14
--- /dev/null
+++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "autoname": "field:company",
+ "creation": "2020-09-25 12:48:51.463265",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "uae_vat_accounts"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "uae_vat_accounts",
+ "fieldtype": "Table",
+ "label": "UAE VAT Accounts",
+ "options": "UAE VAT Account",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-09-30 20:08:18.764798",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "UAE VAT Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.py b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.py
new file mode 100644
index 0000000000..20dc604510
--- /dev/null
+++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.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 UAEVATSettings(Document):
+ pass
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 6164e066cd..5458001e9d 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -516,6 +516,9 @@ def get_address_details(data, doc, company_address, billing_address):
data.transType = 1
data.actualToStateCode = data.toStateCode
shipping_address = billing_address
+
+ if doc.gst_category == 'SEZ':
+ data.toStateCode = 99
return data
diff --git a/erpnext/regional/report/uae_vat_201/__init__.py b/erpnext/regional/report/uae_vat_201/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
new file mode 100644
index 0000000000..daa69768c5
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_201/test_uae_vat_201.py
@@ -0,0 +1,239 @@
+# coding=utf-8
+from __future__ import unicode_literals
+
+import erpnext
+import frappe
+from unittest import TestCase
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse_account
+from erpnext.regional.report.uae_vat_201.uae_vat_201 import (
+ get_total_emiratewise,
+ get_tourist_tax_return_total,
+ get_tourist_tax_return_tax,
+ get_zero_rated_total,
+ get_exempt_total,
+ get_standard_rated_expenses_total,
+ get_standard_rated_expenses_tax,
+)
+
+test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"]
+
+class TestUaeVat201(TestCase):
+ def setUp(self):
+ frappe.set_user("Administrator")
+
+ frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company UAE VAT'")
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company UAE VAT'")
+
+ make_company("_Test Company UAE VAT", "_TCUV")
+ set_vat_accounts()
+
+ make_customer()
+
+ make_supplier()
+
+ create_warehouse("_Test UAE VAT Supplier Warehouse", company="_Test Company UAE VAT")
+
+ make_item("_Test UAE VAT Item", properties = {"is_zero_rated": 0, "is_exempt": 0})
+ make_item("_Test UAE VAT Zero Rated Item", properties = {"is_zero_rated": 1, "is_exempt": 0})
+ make_item("_Test UAE VAT Exempt Item", properties = {"is_zero_rated": 0, "is_exempt": 1})
+
+ make_sales_invoices()
+
+ create_purchase_invoices()
+
+ def test_uae_vat_201_report(self):
+ filters = {"company": "_Test Company UAE VAT"}
+ total_emiratewise = get_total_emiratewise(filters)
+ amounts_by_emirate = {}
+ for data in total_emiratewise:
+ emirate, amount, vat = data
+ amounts_by_emirate[emirate] = {
+ "raw_amount": amount,
+ "raw_vat_amount": vat,
+ }
+ self.assertEqual(amounts_by_emirate["Sharjah"]["raw_amount"],100)
+ self.assertEqual(amounts_by_emirate["Sharjah"]["raw_vat_amount"],5)
+ self.assertEqual(amounts_by_emirate["Dubai"]["raw_amount"],200)
+ self.assertEqual(amounts_by_emirate["Dubai"]["raw_vat_amount"],10)
+ self.assertEqual(get_tourist_tax_return_total(filters),100)
+ self.assertEqual(get_tourist_tax_return_tax(filters),2)
+ self.assertEqual(get_zero_rated_total(filters),100)
+ self.assertEqual(get_exempt_total(filters),100)
+ self.assertEqual(get_standard_rated_expenses_total(filters),250)
+ self.assertEqual(get_standard_rated_expenses_tax(filters),1)
+
+def make_company(company_name, abbr):
+ if not frappe.db.exists("Company", company_name):
+ company = frappe.get_doc({
+ "doctype": "Company",
+ "company_name": company_name,
+ "abbr": abbr,
+ "default_currency": "AED",
+ "country": "United Arab Emirates",
+ "create_chart_of_accounts_based_on": "Standard Template",
+ })
+ company.insert()
+ else:
+ company = frappe.get_doc("Company", company_name)
+
+ company.create_default_warehouses()
+
+ if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}):
+ company.create_default_cost_center()
+
+ company.save()
+ return company
+
+def set_vat_accounts():
+ if not frappe.db.exists("UAE VAT Settings", "_Test Company UAE VAT"):
+ vat_accounts = frappe.get_all(
+ "Account",
+ fields=["name"],
+ filters = {
+ "company": "_Test Company UAE VAT",
+ "is_group": 0,
+ "account_type": "Tax"
+ }
+ )
+
+ uae_vat_accounts = []
+ for account in vat_accounts:
+ uae_vat_accounts.append({
+ "doctype": "UAE VAT Account",
+ "account": account.name
+ })
+
+ frappe.get_doc({
+ "company": "_Test Company UAE VAT",
+ "uae_vat_accounts": uae_vat_accounts,
+ "doctype": "UAE VAT Settings",
+ }).insert()
+
+def make_customer():
+ if not frappe.db.exists("Customer", "_Test UAE Customer"):
+ customer = frappe.get_doc({
+ "doctype": "Customer",
+ "customer_name": "_Test UAE Customer",
+ "customer_type": "Company",
+ })
+ customer.insert()
+ else:
+ customer = frappe.get_doc("Customer", "_Test UAE Customer")
+
+def make_supplier():
+ if not frappe.db.exists("Supplier", "_Test UAE Supplier"):
+ frappe.get_doc({
+ "supplier_group": "Local",
+ "supplier_name": "_Test UAE Supplier",
+ "supplier_type": "Individual",
+ "doctype": "Supplier",
+ }).insert()
+
+def create_warehouse(warehouse_name, properties=None, company=None):
+ if not company:
+ company = "_Test Company"
+
+ warehouse_id = erpnext.encode_company_abbr(warehouse_name, company)
+ if not frappe.db.exists("Warehouse", warehouse_id):
+ warehouse = frappe.new_doc("Warehouse")
+ warehouse.warehouse_name = warehouse_name
+ warehouse.parent_warehouse = "All Warehouses - _TCUV"
+ warehouse.company = company
+ warehouse.account = get_warehouse_account(warehouse_name, company)
+ if properties:
+ warehouse.update(properties)
+ warehouse.save()
+ return warehouse.name
+ else:
+ return warehouse_id
+
+def make_item(item_code, properties=None):
+ if frappe.db.exists("Item", item_code):
+ return frappe.get_doc("Item", item_code)
+
+ item = frappe.get_doc({
+ "doctype": "Item",
+ "item_code": item_code,
+ "item_name": item_code,
+ "description": item_code,
+ "item_group": "Products"
+ })
+
+ if properties:
+ item.update(properties)
+
+ item.insert()
+
+ return item
+
+def make_sales_invoices():
+ def make_sales_invoices_wrapper(emirate, item, tax = True, tourist_tax= False):
+ si = create_sales_invoice(
+ company="_Test Company UAE VAT",
+ customer = '_Test UAE Customer',
+ currency = 'AED',
+ warehouse = 'Finished Goods - _TCUV',
+ debit_to = 'Debtors - _TCUV',
+ income_account = 'Sales - _TCUV',
+ expense_account = 'Cost of Goods Sold - _TCUV',
+ cost_center = 'Main - _TCUV',
+ item = item,
+ do_not_save=1
+ )
+ si.vat_emirate = emirate
+ if tax:
+ si.append(
+ "taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "VAT 5% - _TCUV",
+ "cost_center": "Main - _TCUV",
+ "description": "VAT 5% @ 5.0",
+ "rate": 5.0
+ }
+ )
+ if tourist_tax:
+ si.tourist_tax_return = 2
+ si.submit()
+
+ #Define Item Names
+ uae_item = "_Test UAE VAT Item"
+ uae_exempt_item = "_Test UAE VAT Exempt Item"
+ uae_zero_rated_item = "_Test UAE VAT Zero Rated Item"
+
+ #Sales Invoice with standard rated expense in Dubai
+ make_sales_invoices_wrapper('Dubai', uae_item)
+ #Sales Invoice with standard rated expense in Sharjah
+ make_sales_invoices_wrapper('Sharjah', uae_item)
+ #Sales Invoice with Tourist Tax Return
+ make_sales_invoices_wrapper('Dubai', uae_item, True, True)
+ #Sales Invoice with Exempt Item
+ make_sales_invoices_wrapper('Sharjah', uae_exempt_item, False)
+ #Sales Invoice with Zero Rated Item
+ make_sales_invoices_wrapper('Sharjah', uae_zero_rated_item, False)
+
+def create_purchase_invoices():
+ pi = make_purchase_invoice(
+ company="_Test Company UAE VAT",
+ supplier = '_Test UAE Supplier',
+ supplier_warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV',
+ warehouse = '_Test UAE VAT Supplier Warehouse - _TCUV',
+ currency = 'AED',
+ cost_center = 'Main - _TCUV',
+ expense_account = 'Cost of Goods Sold - _TCUV',
+ item = "_Test UAE VAT Item",
+ do_not_save=1,
+ uom = "Nos"
+ )
+ pi.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "VAT 5% - _TCUV",
+ "cost_center": "Main - _TCUV",
+ "description": "VAT 5% @ 5.0",
+ "rate": 5.0
+ })
+
+ pi.recoverable_standard_rated_expenses = 1
+
+ pi.submit()
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.html b/erpnext/regional/report/uae_vat_201/uae_vat_201.html
new file mode 100644
index 0000000000..d9b9968d90
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.html
@@ -0,0 +1,77 @@
+{%
+ var report_columns = report.get_columns_for_print();
+ report_columns = report_columns.filter(col => !col.hidden);
+%}
+
+
+
{%= __(report.report_name) %}
+
+{%= __("VAT on Sales and All Other Outputs") %}
+
+
+
+
+ {%= report_columns[0].label %} |
+ {%= report_columns[1].label %} |
+
+ {% for (let i=2; i{%= report_columns[i].label %}
+ {% } %}
+
+
+
+ {% for (let j=1; j<12; j++) { %}
+ {%
+ var row = data[j];
+ %}
+
+ {% for (let i=0; i
+ {% const fieldname = report_columns[i].fieldname; %}
+ {% if (!is_null(row[fieldname])) { %}
+ {%= frappe.format(row[fieldname], report_columns[i], {}, row) %}
+ {% } %}
+
+ {% } %}
+
+ {% } %}
+
+
+
+{%= __("VAT on Expenses and All Other Inputs") %}
+
+
+
+ {%= report_columns[0].label %} |
+ {%= report_columns[1].label %} |
+
+ {% for (let i=2; i{%= report_columns[i].label %}
+ {% } %}
+
+
+
+ {% for (let j=14; j
+ {% for (let i=0; i
+ {% const fieldname = report_columns[i].fieldname; %}
+ {% if (!is_null(row[fieldname])) { %}
+ {%= frappe.format(row[fieldname], report_columns[i], {}, row) %}
+ {% } %}
+
+ {% } %}
+
+ {% } %}
+
+
+
\ No newline at end of file
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.js b/erpnext/regional/report/uae_vat_201/uae_vat_201.js
new file mode 100644
index 0000000000..5957424770
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["UAE VAT 201"] = {
+ "filters": [
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -3),
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.get_today()
+ },
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ if (data
+ && (data.legend=='VAT on Sales and All Other Outputs' || data.legend=='VAT on Expenses and All Other Inputs')
+ && data.legend==value) {
+ value = $(`${value}`);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("").parent().html();
+ }
+ return value;
+ },
+};
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.json b/erpnext/regional/report/uae_vat_201/uae_vat_201.json
new file mode 100644
index 0000000000..8a88bcd3e2
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.json
@@ -0,0 +1,22 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-09-10 08:51:02.298482",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-09-10 08:51:02.298482",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "UAE VAT 201",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "UAE VAT 201",
+ "report_type": "Script Report",
+ "roles": []
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/uae_vat_201/uae_vat_201.py b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
new file mode 100644
index 0000000000..b0614238ba
--- /dev/null
+++ b/erpnext/regional/report/uae_vat_201/uae_vat_201.py
@@ -0,0 +1,339 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+
+def execute(filters=None):
+ columns = get_columns()
+ data, emirates, amounts_by_emirate = get_data(filters)
+ return columns, data
+
+def get_columns():
+ """Creates a list of dictionaries that are used to generate column headers of the data table."""
+ return [
+ {
+ "fieldname": "no",
+ "label": _("No"),
+ "fieldtype": "Data",
+ "width": 50
+ },
+ {
+ "fieldname": "legend",
+ "label": _("Legend"),
+ "fieldtype": "Data",
+ "width": 300
+ },
+ {
+ "fieldname": "amount",
+ "label": _("Amount (AED)"),
+ "fieldtype": "Currency",
+ "width": 125,
+ },
+ {
+ "fieldname": "vat_amount",
+ "label": _("VAT Amount (AED)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ }
+ ]
+
+def get_data(filters = None):
+ """Returns the list of dictionaries. Each dictionary is a row in the datatable and chart data."""
+ data = []
+ emirates, amounts_by_emirate = append_vat_on_sales(data, filters)
+ append_vat_on_expenses(data, filters)
+ return data, emirates, amounts_by_emirate
+
+def append_vat_on_sales(data, filters):
+ """Appends Sales and All Other Outputs."""
+ append_data(data, '', _('VAT on Sales and All Other Outputs'), '', '')
+
+ emirates, amounts_by_emirate = standard_rated_expenses_emiratewise(data, filters)
+
+ append_data(data, '2',
+ _('Tax Refunds provided to Tourists under the Tax Refunds for Tourists Scheme'),
+ frappe.format((-1) * get_tourist_tax_return_total(filters), 'Currency'),
+ frappe.format((-1) * get_tourist_tax_return_tax(filters), 'Currency'))
+
+ append_data(data, '3', _('Supplies subject to the reverse charge provision'),
+ frappe.format(get_reverse_charge_total(filters), 'Currency'),
+ frappe.format(get_reverse_charge_tax(filters), 'Currency'))
+
+ append_data(data, '4', _('Zero Rated'),
+ frappe.format(get_zero_rated_total(filters), 'Currency'), "-")
+
+ append_data(data, '5', _('Exempt Supplies'),
+ frappe.format(get_exempt_total(filters), 'Currency'),"-")
+
+ append_data(data, '', '', '', '')
+
+ return emirates, amounts_by_emirate
+
+def standard_rated_expenses_emiratewise(data, filters):
+ """Append emiratewise standard rated expenses and vat."""
+ total_emiratewise = get_total_emiratewise(filters)
+ emirates = get_emirates()
+ amounts_by_emirate = {}
+ for emirate, amount, vat in total_emiratewise:
+ amounts_by_emirate[emirate] = {
+ "legend": emirate,
+ "raw_amount": amount,
+ "raw_vat_amount": vat,
+ "amount": frappe.format(amount, 'Currency'),
+ "vat_amount": frappe.format(vat, 'Currency'),
+ }
+ amounts_by_emirate = append_emiratewise_expenses(data, emirates, amounts_by_emirate)
+ return emirates, amounts_by_emirate
+
+def append_emiratewise_expenses(data, emirates, amounts_by_emirate):
+ """Append emiratewise standard rated expenses and vat."""
+ for no, emirate in enumerate(emirates, 97):
+ if emirate in amounts_by_emirate:
+ amounts_by_emirate[emirate]["no"] = _('1{0}').format(chr(no))
+ amounts_by_emirate[emirate]["legend"] = _('Standard rated supplies in {0}').format(emirate)
+ data.append(amounts_by_emirate[emirate])
+ else:
+ append_data(data, _('1{0}').format(chr(no)),
+ _('Standard rated supplies in {0}').format(emirate),
+ frappe.format(0, 'Currency'), frappe.format(0, 'Currency'))
+ return amounts_by_emirate
+
+def append_vat_on_expenses(data, filters):
+ """Appends Expenses and All Other Inputs."""
+ append_data(data, '', _('VAT on Expenses and All Other Inputs'), '', '')
+ append_data(data, '9', _('Standard Rated Expenses'),
+ frappe.format(get_standard_rated_expenses_total(filters), 'Currency'),
+ frappe.format(get_standard_rated_expenses_tax(filters), 'Currency'))
+ append_data(data, '10', _('Supplies subject to the reverse charge provision'),
+ frappe.format(get_reverse_charge_recoverable_total(filters), 'Currency'),
+ frappe.format(get_reverse_charge_recoverable_tax(filters), 'Currency'))
+
+def append_data(data, no, legend, amount, vat_amount):
+ """Returns data with appended value."""
+ data.append({"no": no, "legend":legend, "amount": amount, "vat_amount": vat_amount})
+
+def get_total_emiratewise(filters):
+ """Returns Emiratewise Amount and Taxes."""
+ conditions = get_conditions(filters)
+ try:
+ return frappe.db.sql("""
+ select
+ s.vat_emirate as emirate, sum(i.base_amount) as total, sum(s.total_taxes_and_charges)
+ from
+ `tabSales Invoice Item` i inner join `tabSales Invoice` s
+ on
+ i.parent = s.name
+ where
+ s.docstatus = 1 and i.is_exempt != 1 and i.is_zero_rated != 1
+ {where_conditions}
+ group by
+ s.vat_emirate;
+ """.format(where_conditions=conditions), filters)
+ except (IndexError, TypeError):
+ return 0
+
+def get_emirates():
+ """Returns a List of emirates in the order that they are to be displayed."""
+ return [
+ 'Abu Dhabi',
+ 'Dubai',
+ 'Sharjah',
+ 'Ajman',
+ 'Umm Al Quwain',
+ 'Ras Al Khaimah',
+ 'Fujairah'
+ ]
+
+def get_filters(filters):
+ """The conditions to be used to filter data to calculate the total sale."""
+ query_filters = []
+ if filters.get("company"):
+ query_filters.append(["company", '=', filters['company']])
+ if filters.get("from_date"):
+ query_filters.append(["posting_date", '>=', filters['from_date']])
+ if filters.get("from_date"):
+ query_filters.append(["posting_date", '<=', filters['to_date']])
+ return query_filters
+
+def get_reverse_charge_total(filters):
+ """Returns the sum of the total of each Purchase invoice made."""
+ query_filters = get_filters(filters)
+ query_filters.append(['reverse_charge', '=', 'Y'])
+ query_filters.append(['docstatus', '=', 1])
+ try:
+ return frappe.db.get_all('Purchase Invoice',
+ filters = query_filters,
+ fields = ['sum(total)'],
+ as_list=True,
+ limit = 1
+ )[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_reverse_charge_tax(filters):
+ """Returns the sum of the tax of each Purchase invoice made."""
+ conditions = get_conditions_join(filters)
+ return frappe.db.sql("""
+ select sum(debit) from
+ `tabPurchase Invoice` p inner join `tabGL Entry` gl
+ on
+ gl.voucher_no = p.name
+ where
+ p.reverse_charge = "Y"
+ and p.docstatus = 1
+ and gl.docstatus = 1
+ and account in (select account from `tabUAE VAT Account` where parent=%(company)s)
+ {where_conditions} ;
+ """.format(where_conditions=conditions), filters)[0][0] or 0
+
+def get_reverse_charge_recoverable_total(filters):
+ """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge."""
+ query_filters = get_filters(filters)
+ query_filters.append(['reverse_charge', '=', 'Y'])
+ query_filters.append(['recoverable_reverse_charge', '>', '0'])
+ query_filters.append(['docstatus', '=', 1])
+ try:
+ return frappe.db.get_all('Purchase Invoice',
+ filters = query_filters,
+ fields = ['sum(total)'],
+ as_list=True,
+ limit = 1
+ )[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_reverse_charge_recoverable_tax(filters):
+ """Returns the sum of the tax of each Purchase invoice made."""
+ conditions = get_conditions_join(filters)
+ return frappe.db.sql("""
+ select
+ sum(debit * p.recoverable_reverse_charge / 100)
+ from
+ `tabPurchase Invoice` p inner join `tabGL Entry` gl
+ on
+ gl.voucher_no = p.name
+ where
+ p.reverse_charge = "Y"
+ and p.docstatus = 1
+ and p.recoverable_reverse_charge > 0
+ and gl.docstatus = 1
+ and account in (select account from `tabUAE VAT Account` where parent=%(company)s)
+ {where_conditions} ;
+ """.format(where_conditions=conditions), filters)[0][0] or 0
+
+def get_conditions_join(filters):
+ """The conditions to be used to filter data to calculate the total vat."""
+ conditions = ""
+ for opts in (("company", " and p.company=%(company)s"),
+ ("from_date", " and p.posting_date>=%(from_date)s"),
+ ("to_date", " and p.posting_date<=%(to_date)s")):
+ if filters.get(opts[0]):
+ conditions += opts[1]
+ return conditions
+
+def get_standard_rated_expenses_total(filters):
+ """Returns the sum of the total of each Purchase invoice made with recoverable reverse charge."""
+ query_filters = get_filters(filters)
+ query_filters.append(['recoverable_standard_rated_expenses', '>', 0])
+ query_filters.append(['docstatus', '=', 1])
+ try:
+ return frappe.db.get_all('Purchase Invoice',
+ filters = query_filters,
+ fields = ['sum(total)'],
+ as_list=True,
+ limit = 1
+ )[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_standard_rated_expenses_tax(filters):
+ """Returns the sum of the tax of each Purchase invoice made."""
+ query_filters = get_filters(filters)
+ query_filters.append(['recoverable_standard_rated_expenses', '>', 0])
+ query_filters.append(['docstatus', '=', 1])
+ try:
+ return frappe.db.get_all('Purchase Invoice',
+ filters = query_filters,
+ fields = ['sum(recoverable_standard_rated_expenses)'],
+ as_list=True,
+ limit = 1
+ )[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_tourist_tax_return_total(filters):
+ """Returns the sum of the total of each Sales invoice with non zero tourist_tax_return."""
+ query_filters = get_filters(filters)
+ query_filters.append(['tourist_tax_return', '>', 0])
+ query_filters.append(['docstatus', '=', 1])
+ try:
+ return frappe.db.get_all('Sales Invoice',
+ filters = query_filters,
+ fields = ['sum(total)'],
+ as_list=True,
+ limit = 1
+ )[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_tourist_tax_return_tax(filters):
+ """Returns the sum of the tax of each Sales invoice with non zero tourist_tax_return."""
+ query_filters = get_filters(filters)
+ query_filters.append(['tourist_tax_return', '>', 0])
+ query_filters.append(['docstatus', '=', 1])
+ try:
+ return frappe.db.get_all('Sales Invoice',
+ filters = query_filters,
+ fields = ['sum(tourist_tax_return)'],
+ as_list=True,
+ limit = 1
+ )[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_zero_rated_total(filters):
+ """Returns the sum of each Sales Invoice Item Amount which is zero rated."""
+ conditions = get_conditions(filters)
+ try:
+ return frappe.db.sql("""
+ select
+ sum(i.base_amount) as total
+ from
+ `tabSales Invoice Item` i inner join `tabSales Invoice` s
+ on
+ i.parent = s.name
+ where
+ s.docstatus = 1 and i.is_zero_rated = 1
+ {where_conditions} ;
+ """.format(where_conditions=conditions), filters)[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+
+def get_exempt_total(filters):
+ """Returns the sum of each Sales Invoice Item Amount which is Vat Exempt."""
+ conditions = get_conditions(filters)
+ try:
+ return frappe.db.sql("""
+ select
+ sum(i.base_amount) as total
+ from
+ `tabSales Invoice Item` i inner join `tabSales Invoice` s
+ on
+ i.parent = s.name
+ where
+ s.docstatus = 1 and i.is_exempt = 1
+ {where_conditions} ;
+ """.format(where_conditions=conditions), filters)[0][0] or 0
+ except (IndexError, TypeError):
+ return 0
+def get_conditions(filters):
+ """The conditions to be used to filter data to calculate the total sale."""
+ conditions = ""
+ for opts in (("company", " and company=%(company)s"),
+ ("from_date", " and posting_date>=%(from_date)s"),
+ ("to_date", " and posting_date<=%(to_date)s")):
+ if filters.get(opts[0]):
+ conditions += opts[1]
+ return conditions
diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py
index 250659e54d..013ae5cf73 100644
--- a/erpnext/regional/united_arab_emirates/setup.py
+++ b/erpnext/regional/united_arab_emirates/setup.py
@@ -5,24 +5,30 @@ from __future__ import unicode_literals
import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.permissions import add_permission, update_permission_property
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
def setup(company=None, patch=True):
make_custom_fields()
add_print_formats()
-
+ add_custom_roles_for_reports()
+ add_permissions()
if company:
create_sales_tax(company)
def make_custom_fields():
+ is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
+ fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description',
+ print_hide=1)
+ is_exempt = dict(fieldname='is_exempt', label='Is Exempt',
+ fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated',
+ print_hide=1)
+
invoice_fields = [
dict(fieldname='vat_section', label='VAT Details', fieldtype='Section Break',
insert_after='group_same_items', print_hide=1, collapsible=1),
dict(fieldname='permit_no', label='Permit Number',
fieldtype='Data', insert_after='vat_section', print_hide=1),
- dict(fieldname='reverse_charge_applicable', label='Reverse Charge Applicable',
- fieldtype='Select', insert_after='permit_no', print_hide=1,
- options='Y\nN', default='N')
]
purchase_invoice_fields = [
@@ -31,7 +37,16 @@ def make_custom_fields():
fetch_from='company.tax_id', print_hide=1),
dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic',
fieldtype='Read Only', insert_after='supplier_name',
- fetch_from='supplier.supplier_name_in_arabic', print_hide=1)
+ fetch_from='supplier.supplier_name_in_arabic', print_hide=1),
+ dict(fieldname='recoverable_standard_rated_expenses', print_hide=1, default='0',
+ label='Recoverable Standard Rated Expenses (AED)', insert_after='permit_no',
+ fieldtype='Currency', ),
+ dict(fieldname='reverse_charge', label='Reverse Charge Applicable',
+ fieldtype='Select', insert_after='recoverable_standard_rated_expenses', print_hide=1,
+ options='Y\nN', default='N'),
+ dict(fieldname='recoverable_reverse_charge', label='Recoverable Reverse Charge (Percentage)',
+ insert_after='reverse_charge', fieldtype='Percent', print_hide=1,
+ depends_on="eval:doc.reverse_charge=='Y'", default='100.000'),
]
sales_invoice_fields = [
@@ -41,6 +56,11 @@ def make_custom_fields():
dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic',
fieldtype='Read Only', insert_after='customer_name',
fetch_from='customer.customer_name_in_arabic', print_hide=1),
+ dict(fieldname='vat_emirate', label='VAT Emirate', insert_after='permit_no', fieldtype='Select',
+ options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain',
+ fetch_from='company_address.emirate'),
+ dict(fieldname='tourist_tax_return', label='Tax Refund provided to Tourists (AED)',
+ insert_after='vat_emirate', fieldtype='Currency', print_hide=1, default='0'),
]
invoice_item_fields = [
@@ -67,6 +87,12 @@ def make_custom_fields():
'Item': [
dict(fieldname='tax_code', label='Tax Code',
fieldtype='Data', insert_after='item_group'),
+ dict(fieldname='is_zero_rated', label='Is Zero Rated',
+ fieldtype='Check', insert_after='tax_code',
+ print_hide=1),
+ dict(fieldname='is_exempt', label='Is Exempt',
+ fieldtype='Check', insert_after='is_zero_rated',
+ print_hide=1)
],
'Customer': [
dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic',
@@ -76,13 +102,17 @@ def make_custom_fields():
dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic',
fieldtype='Data', insert_after='supplier_name'),
],
+ 'Address': [
+ dict(fieldname='emirate', label='Emirate', fieldtype='Select', insert_after='state',
+ options='\nAbu Dhabi\nAjman\nDubai\nFujairah\nRas Al Khaimah\nSharjah\nUmm Al Quwain')
+ ],
'Purchase Invoice': purchase_invoice_fields + invoice_fields,
'Purchase Order': purchase_invoice_fields + invoice_fields,
'Purchase Receipt': purchase_invoice_fields + invoice_fields,
'Sales Invoice': sales_invoice_fields + invoice_fields,
'Sales Order': sales_invoice_fields + invoice_fields,
'Delivery Note': sales_invoice_fields + invoice_fields,
- 'Sales Invoice Item': invoice_item_fields + delivery_date_field,
+ 'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt],
'Purchase Invoice Item': invoice_item_fields,
'Sales Order Item': invoice_item_fields,
'Delivery Note Item': invoice_item_fields,
@@ -101,3 +131,25 @@ def add_print_formats():
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
name in('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice') """)
+
+def add_custom_roles_for_reports():
+ """Add Access Control to UAE VAT 201."""
+ if not frappe.db.get_value('Custom Role', dict(report='UAE VAT 201')):
+ frappe.get_doc(dict(
+ doctype='Custom Role',
+ report='UAE VAT 201',
+ roles= [
+ dict(role='Accounts User'),
+ dict(role='Accounts Manager'),
+ dict(role='Auditor')
+ ]
+ )).insert()
+
+def add_permissions():
+ """Add Permissions for UAE VAT Settings and UAE VAT Account."""
+ for doctype in ('UAE VAT Settings', 'UAE VAT Account'):
+ add_permission(doctype, 'All', 0)
+ for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
+ add_permission(doctype, role, 0)
+ update_permission_property(doctype, role, 0, 'write', 1)
+ update_permission_property(doctype, role, 0, 'create', 1)
diff --git a/erpnext/regional/united_arab_emirates/utils.py b/erpnext/regional/united_arab_emirates/utils.py
index a0425f6b1c..7d5fd6ecf8 100644
--- a/erpnext/regional/united_arab_emirates/utils.py
+++ b/erpnext/regional/united_arab_emirates/utils.py
@@ -1,6 +1,8 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import flt
+from frappe import _
+import erpnext
+from frappe.utils import flt, round_based_on_smallest_currency_fraction, money_in_words
from erpnext.controllers.taxes_and_totals import get_itemised_tax
from six import iteritems
@@ -26,4 +28,134 @@ def update_itemised_tax_data(doc):
row.tax_rate = flt(tax_rate, row.precision("tax_rate"))
row.tax_amount = flt((row.net_amount * tax_rate) / 100, row.precision("net_amount"))
- row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
\ No newline at end of file
+ row.total_amount = flt((row.net_amount + row.tax_amount), row.precision("total_amount"))
+
+def get_account_currency(account):
+ """Helper function to get account currency."""
+ if not account:
+ return
+ def generator():
+ account_currency, company = frappe.get_cached_value(
+ "Account",
+ account,
+ ["account_currency",
+ "company"]
+ )
+ if not account_currency:
+ account_currency = frappe.get_cached_value('Company', company, "default_currency")
+
+ return account_currency
+
+ return frappe.local_cache("account_currency", account, generator)
+
+def get_tax_accounts(company):
+ """Get the list of tax accounts for a specific company."""
+ tax_accounts_dict = frappe._dict()
+ tax_accounts_list = frappe.get_all("UAE VAT Account",
+ filters={"parent": company},
+ fields=["Account"]
+ )
+
+ if not tax_accounts_list and not frappe.flags.in_test:
+ frappe.throw(_('Please set Vat Accounts for Company: "{0}" in UAE VAT Settings').format(company))
+ for tax_account in tax_accounts_list:
+ for account, name in tax_account.items():
+ tax_accounts_dict[name] = name
+
+ return tax_accounts_dict
+
+def update_grand_total_for_rcm(doc, method):
+ """If the Reverse Charge is Applicable subtract the tax amount from the grand total and update in the form."""
+ country = frappe.get_cached_value('Company', doc.company, 'country')
+
+ if country != 'United Arab Emirates':
+ return
+
+ if not doc.total_taxes_and_charges:
+ return
+
+ if doc.reverse_charge == 'Y':
+ tax_accounts = get_tax_accounts(doc.company)
+
+ base_vat_tax = 0
+ vat_tax = 0
+
+ for tax in doc.get('taxes'):
+ if tax.category not in ("Total", "Valuation and Total"):
+ continue
+
+ if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts:
+ base_vat_tax += tax.base_tax_amount_after_discount_amount
+ vat_tax += tax.tax_amount_after_discount_amount
+
+ doc.taxes_and_charges_added -= vat_tax
+ doc.total_taxes_and_charges -= vat_tax
+ doc.base_taxes_and_charges_added -= base_vat_tax
+ doc.base_total_taxes_and_charges -= base_vat_tax
+
+ update_totals(vat_tax, base_vat_tax, doc)
+
+def update_totals(vat_tax, base_vat_tax, doc):
+ """Update the grand total values in the form."""
+ doc.base_grand_total -= base_vat_tax
+ doc.grand_total -= vat_tax
+
+ if doc.meta.get_field("rounded_total"):
+
+ if doc.is_rounded_total_disabled():
+ doc.outstanding_amount = doc.grand_total
+
+ else:
+ doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
+ doc.currency, doc.precision("rounded_total"))
+ doc.rounding_adjustment = flt(doc.rounded_total - doc.grand_total,
+ doc.precision("rounding_adjustment"))
+ doc.outstanding_amount = doc.rounded_total or doc.grand_total
+
+ doc.in_words = money_in_words(doc.grand_total, doc.currency)
+ doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
+ doc.set_payment_schedule()
+
+def make_regional_gl_entries(gl_entries, doc):
+ """Hooked to make_regional_gl_entries in Purchase Invoice.It appends the region specific general ledger entries to the list of GL Entries."""
+ country = frappe.get_cached_value('Company', doc.company, 'country')
+
+ if country != 'United Arab Emirates':
+ return gl_entries
+
+ if doc.reverse_charge == 'Y':
+ tax_accounts = get_tax_accounts(doc.company)
+ for tax in doc.get('taxes'):
+ if tax.category not in ("Total", "Valuation and Total"):
+ continue
+ gl_entries = make_gl_entry(tax, gl_entries, doc, tax_accounts)
+ return gl_entries
+
+def make_gl_entry(tax, gl_entries, doc, tax_accounts):
+ dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
+ if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in tax_accounts:
+ account_currency = get_account_currency(tax.account_head)
+
+ gl_entries.append(doc.get_gl_dict({
+ "account": tax.account_head,
+ "cost_center": tax.cost_center,
+ "posting_date": doc.posting_date,
+ "against": doc.supplier,
+ dr_or_cr: tax.base_tax_amount_after_discount_amount,
+ dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
+ if account_currency==doc.company_currency \
+ else tax.tax_amount_after_discount_amount
+ }, account_currency, item=tax
+ ))
+ return gl_entries
+
+
+def validate_returns(doc, method):
+ """Standard Rated expenses should not be set when Reverse Charge Applicable is set."""
+ country = frappe.get_cached_value('Company', doc.company, 'country')
+ if country != 'United Arab Emirates':
+ return
+ if doc.reverse_charge == 'Y' and flt(doc.recoverable_standard_rated_expenses) != 0:
+ frappe.throw(_(
+ "Recoverable Standard Rated expenses should not be set when Reverse Charge Applicable is Y"
+ ))
diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json
index 5b85187ccb..3eba62bc19 100644
--- a/erpnext/selling/doctype/quotation/quotation.json
+++ b/erpnext/selling/doctype/quotation/quotation.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:08",
@@ -932,7 +933,7 @@
"is_submittable": 1,
"links": [],
"max_attachments": 1,
- "modified": "2020-07-26 17:46:19.951223",
+ "modified": "2020-10-30 13:58:59.212060",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 77c1787c26..3d64ac3780 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-06-18 12:39:59",
@@ -1460,7 +1461,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-07 14:30:01.782617",
+ "modified": "2020-10-30 13:59:18.628077",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index ec1c82339b..04d85e575c 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -844,7 +844,8 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"contact_email",
"contact_person",
"taxes_and_charges",
- "shipping_address"
+ "shipping_address",
+ "terms"
],
"validation": {
"docstatus": ["=", 1]
@@ -863,7 +864,10 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"field_no_map": [
"rate",
"price_list_rate",
- "item_tax_template"
+ "item_tax_template",
+ "discount_percentage",
+ "discount_amount",
+ "pricing_rules"
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
@@ -917,7 +921,8 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"contact_email",
"contact_person",
"taxes_and_charges",
- "shipping_address"
+ "shipping_address",
+ "terms"
],
"validation": {
"docstatus": ["=", 1]
@@ -937,7 +942,10 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"rate",
"price_list_rate",
"item_tax_template",
- "supplier"
+ "discount_percentage",
+ "discount_amount",
+ "supplier",
+ "pricing_rules"
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.item_code in items_to_map
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.py b/erpnext/selling/page/point_of_sale/point_of_sale.py
index a690050f79..062cba19e6 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.py
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.py
@@ -62,6 +62,7 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_va
`tabItem` item {bin_join_selection}
WHERE
item.disabled = 0
+ AND item.is_stock_item = 1
AND item.has_variants = 0
AND item.is_sales_item = 1
AND item.is_fixed_asset = 0
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index b30bd7814b..cbb4c7c5de 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -48,12 +48,8 @@ class EmailDigest(Document):
recipients = list(filter(lambda r: r in valid_users,
self.recipient_list.split("\n")))
- original_user = frappe.session.user
-
if recipients:
for user_id in recipients:
- frappe.set_user(user_id)
- frappe.set_user_lang(user_id)
msg_for_this_recipient = self.get_msg_html()
if msg_for_this_recipient:
frappe.sendmail(
@@ -64,9 +60,6 @@ class EmailDigest(Document):
reference_name = self.name,
unsubscribe_message = _("Unsubscribe from this Email Digest"))
- frappe.set_user(original_user)
- frappe.set_user_lang(original_user)
-
def get_msg_html(self):
"""Build email digest content"""
frappe.flags.ignore_account_permission = True
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 3c5129b1ab..7393c8a70e 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:09",
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index 51b47c50a3..bed5ea9ab6 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -4,14 +4,13 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-
-
-class ItemPriceDuplicateItem(frappe.ValidationError): pass
-
-
from frappe.model.document import Document
+class ItemPriceDuplicateItem(frappe.ValidationError):
+ pass
+
+
class ItemPrice(Document):
def validate(self):
@@ -23,7 +22,7 @@ class ItemPrice(Document):
def validate_item(self):
if not frappe.db.exists("Item", self.item_code):
- frappe.throw(_("Item {0} not found").format(self.item_code))
+ frappe.throw(_("Item {0} not found.").format(self.item_code))
def validate_dates(self):
if self.valid_from and self.valid_upto:
@@ -38,40 +37,45 @@ class ItemPrice(Document):
if not price_list_details:
link = frappe.utils.get_link_to_form('Price List', self.price_list)
- frappe.throw("The price list {0} does not exists or disabled".
- format(link))
+ frappe.throw("The price list {0} does not exist or is disabled".format(link))
self.buying, self.selling, self.currency = price_list_details
def update_item_details(self):
if self.item_code:
- self.item_name, self.item_description = frappe.db.get_value("Item",
- self.item_code,["item_name", "description"])
+ self.item_name, self.item_description = frappe.db.get_value("Item", self.item_code,["item_name", "description"])
def check_duplicates(self):
- conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s"
- condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name)
+ conditions = """where item_code = %(item_code)s and price_list = %(price_list)s and name != %(name)s"""
- for field in ['uom', 'valid_from',
- 'valid_upto', 'packing_unit', 'customer', 'supplier']:
+ for field in [
+ "uom",
+ "valid_from",
+ "valid_upto",
+ "packing_unit",
+ "customer",
+ "supplier",]:
if self.get(field):
- conditions += " and {0} = %({1})s".format(field, field)
- condition_data_dict[field] = self.get(field)
+ conditions += " and {0} = %({0})s ".format(field)
+ else:
+ conditions += "and (isnull({0}) or {0} = '')".format(field)
price_list_rate = frappe.db.sql("""
- SELECT price_list_rate
- FROM `tabItem Price`
- {conditions} """.format(conditions=conditions), condition_data_dict)
+ select price_list_rate
+ from `tabItem Price`
+ {conditions}
+ """.format(conditions=conditions),
+ self.as_dict(),)
- if price_list_rate :
- frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem)
+ if price_list_rate:
+ frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,)
def before_save(self):
if self.selling:
self.reference = self.customer
if self.buying:
self.reference = self.supplier
-
+
if self.selling and not self.buying:
# if only selling then remove supplier
self.supplier = None
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 702acc38fe..f3d406eeca 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -138,4 +138,23 @@ class TestItemPrice(unittest.TestCase):
# Valid price list must already exist
self.assertRaises(frappe.ValidationError, doc.save)
+ def test_empty_duplicate_validation(self):
+ # Check if none/empty values are not compared during insert validation
+ doc = frappe.copy_doc(test_records[2])
+ doc.customer = None
+ doc.price_list_rate = 21
+ doc.insert()
+
+ args = {
+ "price_list": doc.price_list,
+ "uom": "_Test UOM",
+ "transaction_date": '2017-04-18',
+ "qty": 7
+ }
+
+ price = get_price_list_rate_for(args, doc.item_code)
+ frappe.db.rollback()
+
+ self.assertEqual(price, 21)
+
test_records = frappe.get_test_records('Item Price')
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index f1e1fd3679..888bc2de47 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -1,88 +1,57 @@
{
- "allow_copy": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "hash",
- "beta": 0,
- "creation": "2013-02-22 01:28:01",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "editable_grid": 1,
+ "actions": [],
+ "autoname": "hash",
+ "creation": "2013-02-22 01:28:01",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "specification",
+ "value",
+ "column_break_3",
+ "acceptance_formula"
+ ],
"fields": [
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "specification",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Parameter",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "specification",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "print_width": "200px",
- "read_only": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "specification",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Parameter",
+ "oldfieldname": "specification",
+ "oldfieldtype": "Data",
+ "print_width": "200px",
+ "reqd": 1,
"width": "200px"
- },
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "value",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Acceptance Criteria",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "value",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Acceptance Criteria",
+ "oldfieldname": "value",
+ "oldfieldtype": "Data"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Simple Python formula based on numeric Readings.
Example 1: reading_1 > 0.2 and reading_1 < 0.5
\nExample 2: (reading_1 + reading_2) / 2 < 10",
+ "fieldname": "acceptance_formula",
+ "fieldtype": "Code",
+ "in_list_view": 1,
+ "label": "Acceptance Criteria Formula"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2016-07-11 03:28:01.074316",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Item Quality Inspection Parameter",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "track_seen": 0
+ ],
+ "idx": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-16 16:33:42.421842",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Item Quality Inspection Parameter",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index ce54fc883f..13c8ceb759 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -1,5 +1,6 @@
{
"actions": [],
+ "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -1109,7 +1110,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-03 23:20:26.381024",
+ "modified": "2020-10-30 14:00:08.347534",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index c3bb514184..399a63a186 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -4,15 +4,20 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
+from frappe.model.mapper import get_mapped_doc
+from frappe import _
+from frappe.utils import flt
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details
-from frappe.model.mapper import get_mapped_doc
class QualityInspection(Document):
def validate(self):
if not self.readings and self.item_code:
self.get_item_specification_details()
+ if self.readings:
+ self.set_status_based_on_acceptance_formula()
+
def get_item_specification_details(self):
if not self.quality_inspection_template:
self.quality_inspection_template = frappe.db.get_value('Item',
@@ -26,6 +31,7 @@ class QualityInspection(Document):
child = self.append('readings', {})
child.specification = d.specification
child.value = d.value
+ child.acceptance_formula = d.acceptance_formula
child.status = "Accepted"
def get_quality_inspection_template(self):
@@ -58,6 +64,29 @@ class QualityInspection(Document):
.format(parent_doc=self.reference_type, child_doc=doctype),
(quality_inspection, self.modified, self.reference_name, self.item_code))
+ def set_status_based_on_acceptance_formula(self):
+ for reading in self.readings:
+ if not reading.acceptance_formula: continue
+
+ condition = reading.acceptance_formula
+ data = {}
+ for i in range(1, 11):
+ field = "reading_" + str(i)
+ data[field] = flt(reading.get(field)) or 0
+
+ try:
+ result = frappe.safe_eval(condition, None, data)
+ reading.status = "Accepted" if result else "Rejected"
+ except SyntaxError:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
+ title=_("Invalid Formula"))
+ except NameError as e:
+ field = frappe.bold(e.args[0].split()[1])
+ frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
+ .format(reading.idx, field),
+ title=_("Invalid Formula"))
+
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index bb535c1f6a..2c40009426 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -7,6 +7,7 @@ import unittest
from frappe.utils import nowdate
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.controllers.stock_controller import QualityInspectionRejectedError, QualityInspectionRequiredError, QualityInspectionNotSubmittedError
# test_records = frappe.get_test_records('Quality Inspection')
@@ -17,10 +18,12 @@ class TestQualityInspection(unittest.TestCase):
frappe.db.set_value("Item", "_Test Item with QA", "inspection_required_before_delivery", 1)
def test_qa_for_delivery(self):
+ make_stock_entry(item_code="_Test Item with QA", target="_Test Warehouse - _TC", qty=1, basic_rate=100)
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+
self.assertRaises(QualityInspectionRequiredError, dn.submit)
- qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected", submit=True)
+ qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, status="Rejected")
dn.reload()
self.assertRaises(QualityInspectionRejectedError, dn.submit)
@@ -28,12 +31,51 @@ class TestQualityInspection(unittest.TestCase):
dn.reload()
dn.submit()
+ qa.cancel()
+ dn.reload()
+ dn.cancel()
+
def test_qa_not_submit(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
- qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, submit = False)
+ qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name, do_not_submit=True)
dn.items[0].quality_inspection = qa.name
self.assertRaises(QualityInspectionNotSubmittedError, dn.submit)
+ qa.delete()
+ dn.delete()
+
+ def test_formula_based_qi_readings(self):
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ readings = [{
+ "specification": "Iron Content",
+ "acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
+ "reading_1": 0.4
+ },
+ {
+ "specification": "Calcium Content",
+ "acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
+ "reading_1": 0.7
+ },
+ {
+ "specification": "Mg Content",
+ "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9",
+ "reading_1": 0.5,
+ "reading_2": 0.7,
+ "reading_3": "random text" # check if random string input causes issues
+ }]
+
+ qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
+ readings=readings, do_not_save=True)
+ qa.save()
+
+ # status must be auto set as per formula
+ self.assertEqual(qa.readings[0].status, "Accepted")
+ self.assertEqual(qa.readings[1].status, "Rejected")
+ self.assertEqual(qa.readings[2].status, "Accepted")
+
+ qa.delete()
+ dn.delete()
+
def create_quality_inspection(**args):
args = frappe._dict(args)
qa = frappe.new_doc("Quality Inspection")
@@ -44,12 +86,18 @@ def create_quality_inspection(**args):
qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1
qa.inspected_by = frappe.session.user
- qa.append("readings", {
- "specification": "Size",
- "status": args.status
- })
- qa.save()
- if args.submit:
- qa.submit()
+
+ readings = args.readings or {"specification": "Size", "status": args.status}
+
+ if isinstance(readings, list):
+ for entry in readings:
+ qa.append("readings", entry)
+ else:
+ qa.append("readings", readings)
+
+ if not args.do_not_save:
+ qa.save()
+ if not args.do_not_submit:
+ qa.submit()
return qa
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index f9f8a71c02..c1976dd1fb 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -1,22 +1,29 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-02-22 01:27:43",
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"specification",
"value",
+ "status",
+ "column_break_4",
+ "acceptance_formula",
+ "section_break_3",
"reading_1",
"reading_2",
"reading_3",
+ "column_break_10",
"reading_4",
"reading_5",
"reading_6",
+ "column_break_14",
"reading_7",
"reading_8",
"reading_9",
- "reading_10",
- "status"
+ "reading_10"
],
"fields": [
{
@@ -124,15 +131,40 @@
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Accepted\nRejected"
+ },
+ {
+ "fieldname": "section_break_3",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Simple Python formula based on numeric Readings.
Example 1: reading_1 > 0.2 and reading_1 < 0.5
\nExample 2: (reading_1 + reading_2) / 2 < 10",
+ "fieldname": "acceptance_formula",
+ "fieldtype": "Code",
+ "label": "Acceptance Criteria Formula"
+ },
+ {
+ "fieldname": "column_break_10",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
- "modified": "2019-07-11 18:48:12.667404",
+ "links": [],
+ "modified": "2020-11-16 16:34:29.947856",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
"owner": "Administrator",
"permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
index 0d9a90312b..e2848469b8 100644
--- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
@@ -12,5 +12,7 @@ class QualityInspectionTemplate(Document):
def get_template_details(template):
if not template: return []
- return frappe.get_all('Item Quality Inspection Parameter', fields=["specification", "value"],
- filters={'parenttype': 'Quality Inspection Template', 'parent': template}, order_by="idx")
\ No newline at end of file
+ return frappe.get_all('Item Quality Inspection Parameter',
+ fields=["specification", "value", "acceptance_formula"],
+ filters={'parenttype': 'Quality Inspection Template', 'parent': template},
+ order_by="idx")
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 3dc806fb43..8aaf7abcbe 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -20,7 +20,8 @@ def execute(filters=None):
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
details = item_dict["details"]
- if not fifo_queue and (not item_dict.get("total_qty")): continue
+
+ if not fifo_queue: continue
average_age = get_average_age(fifo_queue, to_date)
earliest_age = date_diff(to_date, fifo_queue[0][1])