Merge branch 'develop' into auto-attendance-issue

This commit is contained in:
Jannat Patel 2021-10-13 13:28:25 +05:30 committed by GitHub
commit 00f7f97dc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 1491 additions and 315 deletions

View File

@ -1,6 +1,8 @@
[flake8] [flake8]
ignore = ignore =
B007, B007,
B009,
B010,
B950, B950,
E101, E101,
E111, E111,
@ -65,11 +67,6 @@ ignore =
E713, E713,
E712, E712,
enable-extensions =
M90
select =
M511
max-line-length = 200 max-line-length = 200
exclude=.github/helper/semgrep_rules,test_*.py exclude=.github/helper/semgrep_rules,test_*.py

View File

@ -131,3 +131,21 @@ rules:
key `$X` is uselessly assigned twice. This could be a potential bug. key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python] languages: [python]
severity: ERROR severity: ERROR
- id: frappe-manual-commit
patterns:
- pattern: frappe.db.commit()
- pattern-not-inside: |
try:
...
except ...:
...
message: |
Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `// nosemgrep` on the same line.
paths:
exclude:
- "**/patches/**"
- "**/demo/**"
languages: [python]
severity: ERROR

View File

@ -21,9 +21,9 @@ repos:
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [ additional_dependencies: [
'flake8-mutable', 'flake8-bugbear',
] ]
args: ['--select=M511', '--config', '.github/helper/.flake8_strict'] args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$" exclude: ".*setup.py$"
- repo: https://github.com/timothycrosley/isort - repo: https://github.com/timothycrosley/isort

View File

@ -8,6 +8,8 @@ from frappe import _, throw
from frappe.utils import cint, cstr from frappe.utils import cint, cstr
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
import erpnext
class RootNotEditable(frappe.ValidationError): pass class RootNotEditable(frappe.ValidationError): pass
class BalanceMismatchError(frappe.ValidationError): pass class BalanceMismatchError(frappe.ValidationError): pass
@ -196,7 +198,7 @@ class Account(NestedSet):
"company": company, "company": company,
# parent account's currency should be passed down to child account's curreny # parent account's currency should be passed down to child account's curreny
# if it is None, it picks it up from default company currency, which might be unintended # if it is None, it picks it up from default company currency, which might be unintended
"account_currency": self.account_currency, "account_currency": erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company] "parent_account": parent_acc_name_map[company]
}) })
@ -207,8 +209,7 @@ class Account(NestedSet):
# update the parent company's value in child companies # update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account) doc = frappe.get_doc("Account", child_account)
parent_value_changed = False parent_value_changed = False
for field in ['account_type', 'account_currency', for field in ['account_type', 'freeze_account', 'balance_must_be']:
'freeze_account', 'balance_must_be']:
if doc.get(field) != self.get(field): if doc.get(field) != self.get(field):
parent_value_changed = True parent_value_changed = True
doc.set(field, self.get(field)) doc.set(field, self.get(field))

View File

@ -174,7 +174,7 @@
"default": "0", "default": "0",
"fieldname": "automatically_fetch_payment_terms", "fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Automatically Fetch Payment Terms" "label": "Automatically Fetch Payment Terms from Order"
}, },
{ {
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ", "description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@ -282,7 +282,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-08-19 11:17:38.788054", "modified": "2021-10-11 17:42:36.427699",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -16,7 +16,7 @@ class LoyaltyPointEntry(Document):
def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=None): def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=None):
if not expiry_date: if not expiry_date:
date = today() expiry_date = today()
return frappe.db.sql(''' return frappe.db.sql('''
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice

View File

@ -505,12 +505,13 @@ class PaymentEntry(AccountsController):
def validate_received_amount(self): def validate_received_amount(self):
if self.paid_from_account_currency == self.paid_to_account_currency: if self.paid_from_account_currency == self.paid_to_account_currency:
if self.paid_amount != self.received_amount: if self.paid_amount < self.received_amount:
frappe.throw(_("Received Amount cannot be greater than Paid Amount")) frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
def set_received_amount(self): def set_received_amount(self):
self.base_received_amount = self.base_paid_amount self.base_received_amount = self.base_paid_amount
if self.paid_from_account_currency == self.paid_to_account_currency: if self.paid_from_account_currency == self.paid_to_account_currency \
and not self.payment_type == 'Internal Transfer':
self.received_amount = self.paid_amount self.received_amount = self.paid_amount
def set_amounts_after_tax(self): def set_amounts_after_tax(self):
@ -712,10 +713,14 @@ class PaymentEntry(AccountsController):
dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit" dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
for d in self.get("references"): for d in self.get("references"):
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy() gle = party_gl_dict.copy()
gle.update({ gle.update({
"against_voucher_type": d.reference_doctype, "against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name "against_voucher": d.reference_name,
"cost_center": cost_center
}) })
allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate), allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),

View File

@ -149,16 +149,18 @@
"cb_17", "cb_17",
"hold_comment", "hold_comment",
"more_info", "more_info",
"status",
"inter_company_invoice_reference",
"represents_company",
"column_break_147",
"is_internal_supplier",
"accounting_details_section",
"credit_to", "credit_to",
"party_account_currency", "party_account_currency",
"is_opening", "is_opening",
"against_expense_account", "against_expense_account",
"column_break_63", "column_break_63",
"unrealized_profit_loss_account", "unrealized_profit_loss_account",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
"represents_company",
"remarks", "remarks",
"subscription_section", "subscription_section",
"from_date", "from_date",
@ -1171,6 +1173,15 @@
"options": "fa fa-file-text", "options": "fa fa-file-text",
"print_hide": 1 "print_hide": 1
}, },
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"ignore_user_permissions": 1,
"label": "Is Internal Supplier",
"read_only": 1
},
{ {
"fieldname": "credit_to", "fieldname": "credit_to",
"fieldtype": "Link", "fieldtype": "Link",
@ -1196,7 +1207,7 @@
"default": "No", "default": "No",
"fieldname": "is_opening", "fieldname": "is_opening",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Is Opening", "label": "Is Opening Entry",
"oldfieldname": "is_opening", "oldfieldname": "is_opening",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "No\nYes", "options": "No\nYes",
@ -1298,15 +1309,6 @@
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"fetch_from": "supplier.is_internal_supplier",
"fieldname": "is_internal_supplier",
"fieldtype": "Check",
"ignore_user_permissions": 1,
"label": "Is Internal Supplier",
"read_only": 1
},
{ {
"fieldname": "tax_withholding_category", "fieldname": "tax_withholding_category",
"fieldtype": "Link", "fieldtype": "Link",
@ -1395,13 +1397,24 @@
"hidden": 1, "hidden": 1,
"label": "Ignore Default Payment Terms Template", "label": "Ignore Default Payment Terms Template",
"read_only": 1 "read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details",
"print_hide": 1
},
{
"fieldname": "column_break_147",
"fieldtype": "Column Break"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-09-28 13:10:28.351810", "modified": "2021-10-12 20:55:16.145651",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -124,6 +124,13 @@
"total_advance", "total_advance",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total", "disable_rounded_total",
"column_break4",
"write_off_amount",
"base_write_off_amount",
"write_off_outstanding_amount_automatically",
"column_break_74",
"write_off_account",
"write_off_cost_center",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"get_advances", "get_advances",
@ -144,13 +151,6 @@
"column_break_90", "column_break_90",
"change_amount", "change_amount",
"account_for_change_amount", "account_for_change_amount",
"column_break4",
"write_off_amount",
"base_write_off_amount",
"write_off_outstanding_amount_automatically",
"column_break_74",
"write_off_account",
"write_off_cost_center",
"terms_section_break", "terms_section_break",
"tc_name", "tc_name",
"terms", "terms",
@ -161,14 +161,14 @@
"column_break_84", "column_break_84",
"language", "language",
"more_information", "more_information",
"status",
"inter_company_invoice_reference", "inter_company_invoice_reference",
"is_internal_customer",
"represents_company", "represents_company",
"customer_group", "customer_group",
"campaign", "campaign",
"is_discounted",
"col_break23", "col_break23",
"status", "is_internal_customer",
"is_discounted",
"source", "source",
"more_info", "more_info",
"debit_to", "debit_to",
@ -2031,7 +2031,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-10-02 03:36:10.251715", "modified": "2021-10-11 20:19:38.667508",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -1087,8 +1087,6 @@ class TestSalesInvoice(unittest.TestCase):
actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
frappe.db.commit()
self.assertEqual(actual_qty_0 - 5, actual_qty_1) self.assertEqual(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate # outgoing_rate

View File

@ -103,8 +103,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
column.is_tree = true; column.is_tree = true;
} }
value = default_formatter(value, row, column, data); if (data && data.account && column.apply_currency_formatter) {
data.currency = erpnext.get_currency(column.company_name);
}
value = default_formatter(value, row, column, data);
if (!data.parent_account) { if (!data.parent_account) {
value = $(`<span>${value}</span>`); value = $(`<span>${value}</span>`);

View File

@ -3,12 +3,14 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from collections import defaultdict
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, flt, getdate from frappe.utils import cint, flt, getdate
import erpnext
from erpnext.accounts.report.balance_sheet.balance_sheet import ( from erpnext.accounts.report.balance_sheet.balance_sheet import (
check_opening_balance,
get_chart_data, get_chart_data,
get_provisional_profit_loss, get_provisional_profit_loss,
) )
@ -31,7 +33,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_report_summary as get_pl_summary, get_report_summary as get_pl_summary,
) )
from erpnext.accounts.report.utils import convert_to_presentation_currency from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
def execute(filters=None): def execute(filters=None):
@ -42,7 +44,7 @@ def execute(filters=None):
fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year')) fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year'))
companies_column, companies = get_companies(filters) companies_column, companies = get_companies(filters)
columns = get_columns(companies_column) columns = get_columns(companies_column, filters)
if filters.get('report') == "Balance Sheet": if filters.get('report') == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters) data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters)
@ -73,21 +75,24 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity, provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity,
companies, filters.get('company'), company_currency, True) companies, filters.get('company'), company_currency, True)
message, opening_balance = check_opening_balance(asset, liability, equity) message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies)
if opening_balance and round(opening_balance,2) !=0: if opening_balance:
unclosed ={ unclosed = {
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"warn_if_negative": True, "warn_if_negative": True,
"currency": company_currency "currency": company_currency
} }
for company in companies:
unclosed[company] = opening_balance
if provisional_profit_loss:
provisional_profit_loss[company] = provisional_profit_loss[company] - opening_balance
unclosed["total"]=opening_balance for company in companies:
unclosed[company] = opening_balance.get(company)
if provisional_profit_loss and provisional_profit_loss.get(company):
provisional_profit_loss[company] = (
flt(provisional_profit_loss[company]) - flt(opening_balance.get(company))
)
unclosed["total"] = opening_balance.get(company)
data.append(unclosed) data.append(unclosed)
if provisional_profit_loss: if provisional_profit_loss:
@ -102,6 +107,37 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
return data, message, chart, report_summary return data, message, chart, report_summary
def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies):
opening_balance = {}
for company in companies:
opening_value = 0
# opening_value = Aseet - liability - equity
for data in [asset_data, liability_data, equity_data]:
account_name = get_root_account_name(data[0].root_type, company)
opening_value += get_opening_balance(account_name, data, company)
opening_balance[company] = opening_value
if opening_balance:
return _("Previous Financial Year is not closed"), opening_balance
return '', {}
def get_opening_balance(account_name, data, company):
for row in data:
if row.get('account_name') == account_name:
return row.get('company_wise_opening_bal', {}).get(company, 0.0)
def get_root_account_name(root_type, company):
return frappe.get_all(
'Account',
fields=['account_name'],
filters = {'root_type': root_type, 'is_group': 1,
'company': company, 'parent_account': ('is', 'not set')},
as_list=1
)[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters) income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters) company_currency = get_company_currency(filters)
@ -193,30 +229,37 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data["total"] = total data["total"] = total
return data return data
def get_columns(companies): def get_columns(companies, filters):
columns = [{ columns = [
"fieldname": "account", {
"label": _("Account"), "fieldname": "account",
"fieldtype": "Link", "label": _("Account"),
"options": "Account", "fieldtype": "Link",
"width": 300 "options": "Account",
}] "width": 300
}, {
columns.append({ "fieldname": "currency",
"fieldname": "currency", "label": _("Currency"),
"label": _("Currency"), "fieldtype": "Link",
"fieldtype": "Link", "options": "Currency",
"options": "Currency", "hidden": 1
"hidden": 1 }
}) ]
for company in companies: for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append({ columns.append({
"fieldname": company, "fieldname": company,
"label": company, "label": f'{company} ({currency})',
"fieldtype": "Currency", "fieldtype": "Currency",
"options": "currency", "options": "currency",
"width": 150 "width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company
}) })
return columns return columns
@ -236,6 +279,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None
end_date = filters.period_end_date end_date = filters.period_end_date
filters.end_date = end_date
gl_entries_by_account = {} gl_entries_by_account = {}
for root in frappe.db.sql("""select lft, rgt from tabAccount for root in frappe.db.sql("""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1): where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1):
@ -244,9 +289,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
end_date, root.lft, root.rgt, filters, end_date, root.lft, root.rgt, filters,
gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False) gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters) calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
accumulate_values_into_parents(accounts, accounts_by_name, companies) accumulate_values_into_parents(accounts, accounts_by_name, companies)
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency)
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
if out: if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency) add_total_row(out, root_type, balance_must_be, companies, company_currency)
@ -257,7 +303,10 @@ def get_company_currency(filters=None):
return (filters.get('presentation_currency') return (filters.get('presentation_currency')
or frappe.get_cached_value('Company', filters.company, "default_currency")) or frappe.get_cached_value('Company', filters.company, "default_currency"))
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters): def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year):
start_date = (fiscal_year.year_start_date
if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date)
for entries in gl_entries_by_account.values(): for entries in gl_entries_by_account.values():
for entry in entries: for entry in entries:
if entry.account_number: if entry.account_number:
@ -266,15 +315,32 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_d
account_name = entry.account_name account_name = entry.account_name
d = accounts_by_name.get(account_name) d = accounts_by_name.get(account_name)
if d: if d:
debit, credit = 0, 0
for company in companies: for company in companies:
# check if posting date is within the period # check if posting date is within the period
if (entry.company == company or (filters.get('accumulated_in_group_company')) if (entry.company == company or (filters.get('accumulated_in_group_company'))
and entry.company in companies.get(company)): and entry.company in companies.get(company)):
d[company] = d.get(company, 0.0) + flt(entry.debit) - flt(entry.credit) parent_company_currency = erpnext.get_company_currency(d.company)
child_company_currency = erpnext.get_company_currency(entry.company)
debit, credit = flt(entry.debit), flt(entry.credit)
if (not filters.get('presentation_currency')
and entry.company != company
and parent_company_currency != child_company_currency
and filters.get('accumulated_in_group_company')):
debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date)
credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date)
d[company] = d.get(company, 0.0) + flt(debit) - flt(credit)
if entry.posting_date < getdate(start_date):
d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit))
if entry.posting_date < getdate(start_date): if entry.posting_date < getdate(start_date):
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit) d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit)
def accumulate_values_into_parents(accounts, accounts_by_name, companies): def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts""" """accumulate children's values in parent accounts"""
@ -282,17 +348,18 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account: if d.parent_account:
account = d.parent_account_name account = d.parent_account_name
if not accounts_by_name.get(account): # if not accounts_by_name.get(account):
continue # continue
for company in companies: for company in companies:
accounts_by_name[account][company] = \ accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0)
accounts_by_name[account]["opening_balance"] = \ accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, filters)
@ -353,7 +420,7 @@ def get_accounts(root_type, filters):
`tabAccount` where company = %s and root_type = %s `tabAccount` where company = %s and root_type = %s
""" , (filters.get('company'), root_type), as_dict=1) """ , (filters.get('company'), root_type), as_dict=1)
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency): def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = [] data = []
for d in accounts: for d in accounts:
@ -367,10 +434,13 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
"parent_account": _(d.parent_account), "parent_account": _(d.parent_account),
"indent": flt(d.indent), "indent": flt(d.indent),
"year_start_date": start_date, "year_start_date": start_date,
"root_type": d.root_type,
"year_end_date": end_date, "year_end_date": end_date,
"currency": company_currency, "currency": filters.presentation_currency,
"company_wise_opening_bal": d.company_wise_opening_bal,
"opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1) "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1)
}) })
for company in companies: for company in companies:
if d.get(company) and balance_must_be == "Credit": if d.get(company) and balance_must_be == "Credit":
# change sign based on Debit or Credit, since calculation is done using (debit - credit) # change sign based on Debit or Credit, since calculation is done using (debit - credit)
@ -385,6 +455,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
row["has_value"] = has_value row["has_value"] = has_value
row["total"] = total row["total"] = total
data.append(row) data.append(row)
return data return data
@ -447,6 +518,7 @@ def get_account_details(account):
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1) 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
def validate_entries(key, entry, accounts_by_name, accounts): def validate_entries(key, entry, accounts_by_name, accounts):
# If an account present in the child company and not in the parent company
if key not in accounts_by_name: if key not in accounts_by_name:
args = get_account_details(entry.account) args = get_account_details(entry.account)
@ -456,12 +528,23 @@ def validate_entries(key, entry, accounts_by_name, accounts):
args.update({ args.update({
'lft': parent_args.lft + 1, 'lft': parent_args.lft + 1,
'rgt': parent_args.rgt - 1, 'rgt': parent_args.rgt - 1,
'indent': 3,
'root_type': parent_args.root_type, 'root_type': parent_args.root_type,
'report_type': parent_args.report_type 'report_type': parent_args.report_type,
'parent_account_name': parent_args.account_name,
'company_wise_opening_bal': defaultdict(float)
}) })
accounts_by_name.setdefault(key, args) accounts_by_name.setdefault(key, args)
accounts.append(args)
idx = len(accounts)
# To identify parent account index
for index, row in enumerate(accounts):
if row.parent_account_name == args.parent_account_name:
idx = index
break
accounts.insert(idx+1, args)
def get_additional_conditions(from_date, ignore_closing_entries, filters): def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = [] additional_conditions = []
@ -491,7 +574,6 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
for company in companies: for company in companies:
total_row.setdefault(company, 0.0) total_row.setdefault(company, 0.0)
total_row[company] += row.get(company, 0.0) total_row[company] += row.get(company, 0.0)
row[company] = 0.0
total_row.setdefault("total", 0.0) total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"]) total_row["total"] += flt(row["total"])
@ -511,6 +593,7 @@ def filter_accounts(accounts, depth=10):
account_name = d.account_number + ' - ' + d.account_name account_name = d.account_number + ' - ' + d.account_name
else: else:
account_name = d.account_name account_name = d.account_name
d['company_wise_opening_bal'] = defaultdict(float)
accounts_by_name[account_name] = d accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d) parent_children_map.setdefault(d.parent_account or None, []).append(d)

View File

@ -533,6 +533,17 @@
"only_for": "United Arab Emirates", "only_for": "United Arab Emirates",
"type": "Link" "type": "Link"
}, },
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "KSA VAT Report",
"link_to": "KSA VAT",
"link_type": "Report",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -1153,6 +1164,16 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"hidden": 0,
"is_query_report": 0,
"label": "KSA VAT Setting",
"link_to": "KSA VAT Setting",
"link_type": "DocType",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -1206,7 +1227,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-08-27 12:15:52.872470", "modified": "2021-08-26 13:15:52.872470",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",

View File

@ -11,7 +11,7 @@ frappe.tour['Buying Settings'] = [
{ {
fieldname: "supp_master_name", fieldname: "supp_master_name",
title: "Supplier Naming By", title: "Supplier Naming By",
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" choose the 'Naming Series' option."), description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a <a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a> choose the 'Naming Series' option."),
}, },
{ {
fieldname: "buying_price_list", fieldname: "buying_price_list",

View File

@ -0,0 +1,77 @@
{
"creation": "2021-07-28 11:51:42.319984",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-10-05 13:06:56.414584",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
"owner": "Administrator",
"reference_doctype": "Buying Settings",
"save_on_complete": 0,
"steps": [
{
"description": "When a Supplier is saved, system generates a unique identity or name for that Supplier which can be used to refer the Supplier in various Buying transactions.",
"field": "",
"fieldname": "supp_master_name",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Supplier Naming By",
"parent_field": "",
"position": "Bottom",
"title": "Supplier Naming By"
},
{
"description": "Configure what should be the default value of Supplier Group when creating a new Supplier.",
"field": "",
"fieldname": "supplier_group",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Supplier Group",
"parent_field": "",
"position": "Right",
"title": "Default Supplier Group"
},
{
"description": "Item prices will be fetched from this Price List.",
"field": "",
"fieldname": "buying_price_list",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Default Buying Price List",
"parent_field": "",
"position": "Bottom",
"title": "Default Buying Price List"
},
{
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice or a Purchase Receipt directly without creating a Purchase Order first.",
"field": "",
"fieldname": "po_required",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
"parent_field": "",
"position": "Bottom",
"title": "Purchase Order Required"
},
{
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first.",
"field": "",
"fieldname": "pr_required",
"fieldtype": "Select",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
"parent_field": "",
"position": "Bottom",
"title": "Purchase Receipt Required"
}
],
"title": "Buying Settings"
}

View File

@ -0,0 +1,82 @@
{
"creation": "2021-07-29 14:11:58.271113",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-10-05 13:11:31.436135",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
"owner": "Administrator",
"reference_doctype": "Purchase Order",
"save_on_complete": 1,
"steps": [
{
"description": "Select a Supplier",
"field": "",
"fieldname": "supplier",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Supplier",
"parent_field": "",
"position": "Right",
"title": "Supplier"
},
{
"description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
"field": "",
"fieldname": "schedule_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Required By",
"parent_field": "",
"position": "Left",
"title": "Required By"
},
{
"description": "Items to be purchased can be added here.",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Items",
"parent_field": "",
"position": "Bottom",
"title": "Items Table"
},
{
"child_doctype": "Purchase Order Item",
"description": "Enter the Item Code.",
"field": "",
"fieldname": "item_code",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 1,
"label": "Item Code",
"next_step_condition": "eval: doc.item_code",
"parent_field": "",
"parent_fieldname": "items",
"position": "Right",
"title": "Item Code"
},
{
"child_doctype": "Purchase Order Item",
"description": "Enter the required quantity for the material.",
"field": "",
"fieldname": "qty",
"fieldtype": "Float",
"has_next_condition": 0,
"is_table_field": 1,
"label": "Quantity",
"parent_field": "",
"parent_fieldname": "items",
"position": "Bottom",
"title": "Quantity"
}
],
"title": "Purchase Order"
}

View File

@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying", "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"modified": "2020-07-08 14:05:28.273641", "modified": "2021-08-24 18:13:42.463776",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying", "name": "Buying",
@ -28,23 +28,11 @@
{ {
"step": "Introduction to Buying" "step": "Introduction to Buying"
}, },
{
"step": "Create a Supplier"
},
{
"step": "Setup your Warehouse"
},
{
"step": "Create a Product"
},
{ {
"step": "Create a Material Request" "step": "Create a Material Request"
}, },
{ {
"step": "Create your first Purchase Order" "step": "Create your first Purchase Order"
},
{
"step": "Buying Settings"
} }
], ],
"subtitle": "Products, Purchases, Analysis, and more.", "subtitle": "Products, Purchases, Analysis, and more.",

View File

@ -1,19 +1,21 @@
{ {
"action": "Create Entry", "action": "Show Form Tour",
"action_label": "Let\u2019s create your first Material Request",
"creation": "2020-05-15 14:39:09.818764", "creation": "2020-05-15 14:39:09.818764",
"description": "# Track Material Request\n\n\nAlso known as Purchase Request or an Indent, is a document identifying a requirement of a set of items (products or services) for various purposes like procurement, transfer, issue, or manufacturing. Once the Material Request is validated, a purchase manager can take the next actions for purchasing items like requesting RFQ from a supplier or directly placing an order with an identified Supplier.\n\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 1,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-15 14:39:09.818764", "modified": "2021-08-24 18:08:08.347501",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create a Material Request", "name": "Create a Material Request",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Material Request", "reference_document": "Material Request",
"show_form_tour": 1,
"show_full_form": 1, "show_full_form": 1,
"title": "Create a Material Request", "title": "Track Material Request",
"validate_action": 1 "validate_action": 1
} }

View File

@ -1,19 +1,21 @@
{ {
"action": "Create Entry", "action": "Show Form Tour",
"action_label": "Let\u2019s create your first Purchase Order",
"creation": "2020-05-12 18:17:49.976035", "creation": "2020-05-12 18:17:49.976035",
"description": "# Create first Purchase Order\n\nPurchase Order is at the heart of your buying transactions. In ERPNext, Purchase Order can can be created against a Purchase Material Request (indent) and Supplier Quotation as well. Purchase Orders is also linked to Purchase Receipt and Purchase Invoices, allowing you to keep a birds-eye view on your purchase deals.\n\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-12 18:31:56.856112", "modified": "2021-08-24 18:08:08.936484",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Create your first Purchase Order", "name": "Create your first Purchase Order",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Purchase Order", "reference_document": "Purchase Order",
"show_form_tour": 0,
"show_full_form": 0, "show_full_form": 0,
"title": "Create your first Purchase Order", "title": "Create first Purchase Order",
"validate_action": 1 "validate_action": 1
} }

View File

@ -1,19 +1,22 @@
{ {
"action": "Watch Video", "action": "Show Form Tour",
"action_label": "Let\u2019s walk-through few Buying Settings",
"creation": "2020-05-06 15:37:09.477765", "creation": "2020-05-06 15:37:09.477765",
"description": "# Buying Settings\n\n\nBuying module\u2019s features are highly configurable as per your business needs. Buying Settings is the place where you can set your preferences for:\n\n- Supplier naming and default values\n- Billing and shipping preference in buying transactions\n\n\n",
"docstatus": 0, "docstatus": 0,
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0, "is_single": 1,
"is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-12 18:25:08.509900", "modified": "2021-08-24 18:08:08.345735",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Introduction to Buying", "name": "Introduction to Buying",
"owner": "Administrator", "owner": "Administrator",
"show_full_form": 0, "reference_document": "Buying Settings",
"title": "Introduction to Buying", "show_form_tour": 1,
"show_full_form": 1,
"title": "Buying Settings",
"validate_action": 1, "validate_action": 1,
"video_url": "https://youtu.be/efFajTTQBa8" "video_url": "https://youtu.be/efFajTTQBa8"
} }

View File

@ -45,7 +45,6 @@ class TestProcurementTracker(unittest.TestCase):
pr = make_purchase_receipt(po.name) pr = make_purchase_receipt(po.name)
pr.get("items")[0].cost_center = "Main - _TPC" pr.get("items")[0].cost_center = "Main - _TPC"
pr.submit() pr.submit()
frappe.db.commit()
date_obj = datetime.date(datetime.now()) date_obj = datetime.date(datetime.now())
po.load_from_db() po.load_from_db()

View File

@ -591,7 +591,7 @@ def future_sle_exists(args, sl_entries=None):
data = frappe.db.sql(""" data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row select item_code, warehouse, count(name) as total_row
from `tabStock Ledger Entry` from `tabStock Ledger Entry` force index (item_warehouse)
where where
({}) ({})
and timestamp(posting_date, posting_time) and timestamp(posting_date, posting_time)

View File

@ -250,6 +250,7 @@ doc_events = {
"validate": "erpnext.regional.india.utils.validate_tax_category" "validate": "erpnext.regional.india.utils.validate_tax_category"
}, },
"Sales Invoice": { "Sales Invoice": {
"after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [ "on_submit": [
"erpnext.regional.create_transaction_log", "erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit", "erpnext.regional.italy.utils.sales_invoice_on_submit",
@ -259,7 +260,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel", "erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction" "erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
], ],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": [
"erpnext.regional.check_deletion_permission",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
"validate": [ "validate": [
"erpnext.regional.india.utils.validate_document_name", "erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values" "erpnext.regional.india.utils.update_taxable_values"

View File

@ -74,7 +74,6 @@ class TestDailyWorkSummary(unittest.TestCase):
from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \ from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
where q.name = r.parent""", as_dict=1) where q.name = r.parent""", as_dict=1)
frappe.db.commit()
def setup_groups(self, hour=None): def setup_groups(self, hour=None):
# setup email to trigger at this hour # setup email to trigger at this hour

View File

@ -10,6 +10,26 @@ frappe.ui.form.on('Expense Claim', {
}, },
company: function(frm) { company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype); erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
var expenses = frm.doc.expenses;
for (var i = 0; i < expenses.length; i++) {
var expense = expenses[i];
if (!expense.expense_type) {
continue;
}
frappe.call({
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
args: {
"expense_claim_type": expense.expense_type,
"company": frm.doc.company
},
callback: function(r) {
if (r.message) {
expense.default_account = r.message.account;
expense.cost_center = r.message.cost_center;
}
}
});
}
}, },
}); });

View File

@ -334,7 +334,6 @@
}, },
{ {
"depends_on": "eval:doc.is_secured_loan", "depends_on": "eval:doc.is_secured_loan",
"fetch_from": "loan_application.maximum_loan_amount",
"fieldname": "maximum_loan_amount", "fieldname": "maximum_loan_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Maximum Loan Amount", "label": "Maximum Loan Amount",
@ -360,7 +359,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-19 18:10:32.360818", "modified": "2021-10-12 18:10:32.360818",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Loan", "name": "Loan",

View File

@ -137,16 +137,23 @@ class Loan(AccountsController):
frappe.throw(_("Loan amount is mandatory")) frappe.throw(_("Loan amount is mandatory"))
def link_loan_security_pledge(self): def link_loan_security_pledge(self):
if self.is_secured_loan: if self.is_secured_loan and self.loan_application:
loan_security_pledge = frappe.db.get_value('Loan Security Pledge', {'loan_application': self.loan_application}, maximum_loan_value = frappe.db.get_value('Loan Security Pledge',
'name') {
'loan_application': self.loan_application,
'status': 'Requested'
},
'sum(maximum_loan_value)'
)
if loan_security_pledge: if maximum_loan_value:
frappe.db.set_value('Loan Security Pledge', loan_security_pledge, { frappe.db.sql("""
'loan': self.name, UPDATE `tabLoan Security Pledge`
'status': 'Pledged', SET loan = %s, pledge_time = %s, status = 'Pledged'
'pledge_time': now_datetime() WHERE status = 'Requested' and loan_application = %s
}) """, (self.name, now_datetime(), self.loan_application))
self.db_set('maximum_loan_amount', maximum_loan_value)
def unlink_loan_security_pledge(self): def unlink_loan_security_pledge(self):
pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name}) pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})

View File

@ -130,10 +130,11 @@ class LoanApplication(Document):
def create_loan(source_name, target_doc=None, submit=0): def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent): def update_accounts(source_doc, target_doc, source_parent):
account_details = frappe.get_all("Loan Type", account_details = frappe.get_all("Loan Type",
fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"], fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
filters = {'name': source_doc.loan_type} filters = {'name': source_doc.loan_type})[0]
)[0]
if source_doc.is_secured_loan:
target_doc.maximum_loan_amount = 0
target_doc.mode_of_payment = account_details.mode_of_payment target_doc.mode_of_payment = account_details.mode_of_payment
target_doc.payment_account = account_details.payment_account target_doc.payment_account = account_details.payment_account

View File

@ -198,7 +198,7 @@ def get_disbursal_amount(loan, on_current_security_price=0):
security_value = get_total_pledged_security_value(loan) security_value = get_total_pledged_security_value(loan)
if loan_details.is_secured_loan and not on_current_security_price: if loan_details.is_secured_loan and not on_current_security_price:
security_value = flt(loan_details.maximum_loan_amount) security_value = get_maximum_amount_as_per_pledged_security(loan)
if not security_value and not loan_details.is_secured_loan: if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount) security_value = flt(loan_details.loan_amount)
@ -209,3 +209,6 @@ def get_disbursal_amount(loan, on_current_security_price=0):
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount return disbursal_amount
def get_maximum_amount_as_per_pledged_security(loan):
return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)'))

View File

@ -411,7 +411,7 @@ def get_amounts(amounts, against_loan, posting_date):
if due_date and not final_due_date: if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days) final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'): if against_loan_doc.status in ('Disbursed', 'Closed') or against_loan_doc.disbursed_amount >= against_loan_doc.loan_amount:
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \ pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount - against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
else: else:

View File

@ -1133,8 +1133,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
query_filters["has_variants"] = 0 query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
or_cond_filters["is_stock_item"] = 1 query_filters["is_stock_item"] = 1
or_cond_filters["has_variants"] = 1
return frappe.get_list("Item", return frappe.get_list("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,

View File

@ -4,13 +4,14 @@
import unittest import unittest
from collections import deque from collections import deque
from functools import partial
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.manufacturing.doctype.bom.bom import make_variant_bom from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
@ -375,6 +376,16 @@ class TestBOM(unittest.TestCase):
# FG Items in Scrap/Loss Table should have Is Process Loss set # FG Items in Scrap/Loss Table should have Is Process Loss set
self.assertRaises(frappe.ValidationError, bom_doc.submit) self.assertRaises(frappe.ValidationError, bom_doc.submit)
def test_bom_item_query(self):
query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1})
test_items = query(txt="_Test")
filtered = query(txt="_Test Item 2")
self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
def get_default_bom(item_code="_Test FG Item 2"): def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@ -246,7 +246,7 @@ erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v12_0.add_state_code_for_ladakh erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v12_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
@ -299,7 +299,7 @@ erpnext.patches.v13_0.gst_fields_for_pos_invoice
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.trim_sales_invoice_custom_field_length erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
erpnext.patches.v13_0.create_custom_field_for_finance_book erpnext.patches.v13_0.create_custom_field_for_finance_book
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries #2
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
erpnext.patches.v13_0.add_default_interview_notification_templates erpnext.patches.v13_0.add_default_interview_notification_templates

View File

@ -17,7 +17,7 @@ def execute():
where where
ref_exchange_rate = 1 ref_exchange_rate = 1
and docstatus = 1 and docstatus = 1
and ifnull(exchange_gain_loss, '') != '' and ifnull(exchange_gain_loss, 0) != 0
group by group by
parent parent
""", as_dict=1) """, as_dict=1)
@ -30,7 +30,7 @@ def execute():
where where
ref_exchange_rate = 1 ref_exchange_rate = 1
and docstatus = 1 and docstatus = 1
and ifnull(exchange_gain_loss, '') != '' and ifnull(exchange_gain_loss, 0) != 0
group by group by
parent parent
""", as_dict=1) """, as_dict=1)
@ -38,12 +38,24 @@ def execute():
if purchase_invoices + sales_invoices: if purchase_invoices + sales_invoices:
frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log") frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
if acc_frozen_upto:
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
for invoice in purchase_invoices + sales_invoices: for invoice in purchase_invoices + sales_invoices:
doc = frappe.get_doc(invoice.type, invoice.name) try:
doc.docstatus = 2 doc = frappe.get_doc(invoice.type, invoice.name)
doc.make_gl_entries() doc.docstatus = 2
for advance in doc.advances: doc.make_gl_entries()
if advance.ref_exchange_rate == 1: for advance in doc.advances:
advance.db_set('exchange_gain_loss', 0, False) if advance.ref_exchange_rate == 1:
doc.docstatus = 1 advance.db_set('exchange_gain_loss', 0, False)
doc.make_gl_entries() doc.docstatus = 1
doc.make_gl_entries()
frappe.db.commit()
except Exception:
frappe.db.rollback()
print(f'Failed to correct gl entries of {invoice.name}')
if acc_frozen_upto:
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', acc_frozen_upto)

View File

@ -2,7 +2,7 @@ import frappe
def execute(): def execute():
frappe.reload_doc('custom', 'doctype', 'custom_field') frappe.reload_doc('custom', 'doctype', 'custom_field', force=True)
company = frappe.get_all('Company', filters = {'country': 'India'}) company = frappe.get_all('Company', filters = {'country': 'India'})
if not company: if not company:
return return

View File

@ -31,3 +31,4 @@ def create_transaction_log(doc, method):
"document_name": doc.name, "document_name": doc.name,
"data": data "data": data
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)

View File

@ -6,8 +6,10 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"gst_summary", "gst_summary",
"column_break_2", "gst_tax_settings_section",
"round_off_gst_values", "round_off_gst_values",
"column_break_4",
"hsn_wise_tax_breakup",
"gstin_email_sent_on", "gstin_email_sent_on",
"section_break_4", "section_break_4",
"gst_accounts", "gst_accounts",
@ -17,37 +19,23 @@
{ {
"fieldname": "gst_summary", "fieldname": "gst_summary",
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "GST Summary", "label": "GST Summary"
"show_days": 1,
"show_seconds": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "gstin_email_sent_on", "fieldname": "gstin_email_sent_on",
"fieldtype": "Date", "fieldtype": "Date",
"label": "GSTIN Email Sent On", "label": "GSTIN Email Sent On",
"read_only": 1, "read_only": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "gst_accounts", "fieldname": "gst_accounts",
"fieldtype": "Table", "fieldtype": "Table",
"label": "GST Accounts", "label": "GST Accounts",
"options": "GST Account", "options": "GST Account"
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "250000", "default": "250000",
@ -56,24 +44,35 @@
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "B2C Limit", "label": "B2C Limit",
"reqd": 1, "reqd": 1
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "Enabling this option will round off individual GST components in all the Invoices", "description": "Enabling this option will round off individual GST components in all the Invoices",
"fieldname": "round_off_gst_values", "fieldname": "round_off_gst_values",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Round Off GST Values", "label": "Round Off GST Values"
"show_days": 1, },
"show_seconds": 1 {
"default": "0",
"fieldname": "hsn_wise_tax_breakup",
"fieldtype": "Check",
"label": "Tax Breakup Table Based On HSN Code"
},
{
"fieldname": "gst_tax_settings_section",
"fieldtype": "Section Break",
"label": "GST Tax Settings"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-01-28 17:19:47.969260", "modified": "2021-10-11 18:10:14.242614",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Regional", "module": "Regional",
"name": "GST Settings", "name": "GST Settings",
@ -83,4 +82,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-07-13 09:17:09.862163",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:38.205597",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Purchase Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATPurchaseAccount(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Sales Account', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,49 @@
{
"actions": [],
"creation": "2021-07-13 08:46:33.820968",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title",
"item_tax_template",
"account"
],
"fields": [
{
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Title",
"reqd": 1
},
{
"fieldname": "item_tax_template",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Tax Template",
"options": "Item Tax Template",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-04 06:42:00.081407",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Sales Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSalesAccount(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSalesAccount(unittest.TestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Havenir Solutions and contributors
// For license information, please see license.txt
frappe.ui.form.on('KSA VAT Setting', {
onload: function () {
frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting');
}
});

View File

@ -0,0 +1,49 @@
{
"actions": [],
"autoname": "field:company",
"creation": "2021-07-13 08:49:01.100356",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"ksa_vat_sales_accounts",
"ksa_vat_purchase_accounts"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1,
"unique": 1
},
{
"fieldname": "ksa_vat_sales_accounts",
"fieldtype": "Table",
"label": "KSA VAT Sales Accounts",
"options": "KSA VAT Sales Account",
"reqd": 1
},
{
"fieldname": "ksa_vat_purchase_accounts",
"fieldtype": "Table",
"label": "KSA VAT Purchase Accounts",
"options": "KSA VAT Purchase Account",
"reqd": 1
}
],
"links": [],
"modified": "2021-08-26 04:29:06.499378",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT Setting",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "company",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class KSAVATSetting(Document):
pass

View File

@ -0,0 +1,5 @@
frappe.listview_settings['KSA VAT Setting'] = {
onload () {
frappe.breadcrumbs.add('Accounts');
}
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Havenir Solutions and Contributors
# See license.txt
# import frappe
import unittest
class TestKSAVATSetting(unittest.TestCase):
pass

View File

@ -112,7 +112,11 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label)) frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts): def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
return [_("Item"), _("Taxable Amount")] + tax_accounts hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup')
if frappe.get_meta(item_doctype).has_field('gst_hsn_code') and hsn_wise_in_gst_settings:
return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
else:
return [_("Item"), _("Taxable Amount")] + tax_accounts
def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False): def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise) itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise)
@ -122,14 +126,17 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'): if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'):
return itemised_tax, itemised_taxable_amount return itemised_tax, itemised_taxable_amount
if hsn_wise: hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup')
tax_breakup_hsn_wise = hsn_wise or hsn_wise_in_gst_settings
if tax_breakup_hsn_wise:
item_hsn_map = frappe._dict() item_hsn_map = frappe._dict()
for d in doc.items: for d in doc.items:
item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code")) item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code"))
hsn_tax = {} hsn_tax = {}
for item, taxes in itemised_tax.items(): for item, taxes in itemised_tax.items():
item_or_hsn = item if not hsn_wise else item_hsn_map.get(item) item_or_hsn = item if not tax_breakup_hsn_wise else item_hsn_map.get(item)
hsn_tax.setdefault(item_or_hsn, frappe._dict()) hsn_tax.setdefault(item_or_hsn, frappe._dict())
for tax_desc, tax_detail in taxes.items(): for tax_desc, tax_detail in taxes.items():
key = tax_desc key = tax_desc
@ -142,7 +149,7 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
# set taxable amount # set taxable amount
hsn_taxable_amount = frappe._dict() hsn_taxable_amount = frappe._dict()
for item in itemised_taxable_amount: for item in itemised_taxable_amount:
item_or_hsn = item if not hsn_wise else item_hsn_map.get(item) item_or_hsn = item if not tax_breakup_hsn_wise else item_hsn_map.get(item)
hsn_taxable_amount.setdefault(item_or_hsn, 0) hsn_taxable_amount.setdefault(item_or_hsn, 0)
hsn_taxable_amount[item_or_hsn] += itemised_taxable_amount.get(item) hsn_taxable_amount[item_or_hsn] += itemised_taxable_amount.get(item)

View File

@ -0,0 +1,60 @@
// Copyright (c) 2016, Havenir Solutions and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["KSA VAT"] = {
onload() {
frappe.breadcrumbs.add('Accounts');
},
"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(), -1),
},
{
"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.title=='VAT on Sales' || data.title=='VAT on Purchases')
&& data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else if (data.title=='Grand Total'){
if (data.title==value) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
return value
}else{
value = default_formatter(value, row, column, data);
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();
console.log($value)
return value
}
}else{
value = default_formatter(value, row, column, data);
return value;
}
},
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-13 08:54:38.000949",
"disable_prepared_report": 1,
"disabled": 1,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-08-26 04:14:37.202594",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA VAT",
"owner": "Administrator",
"prepared_report": 1,
"ref_doctype": "GL Entry",
"report_name": "KSA VAT",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "Accounts Manager"
},
{
"role": "Accounts User"
}
]
}

View File

@ -0,0 +1,176 @@
# Copyright (c) 2013, Havenir Solutions and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import json
import frappe
from frappe import _
from frappe.utils import get_url_to_list
def execute(filters=None):
columns = columns = get_columns()
data = get_data(filters)
return columns, data
def get_columns():
return [
{
"fieldname": "title",
"label": _("Title"),
"fieldtype": "Data",
"width": 300
},
{
"fieldname": "amount",
"label": _("Amount (SAR)"),
"fieldtype": "Currency",
"width": 150,
},
{
"fieldname": "adjustment_amount",
"label": _("Adjustment (SAR)"),
"fieldtype": "Currency",
"width": 150,
},
{
"fieldname": "vat_amount",
"label": _("VAT Amount (SAR)"),
"fieldtype": "Currency",
"width": 150,
}
]
def get_data(filters):
data = []
# Validate if vat settings exist
company = filters.get('company')
if frappe.db.exists('KSA VAT Setting', company) is None:
url = get_url_to_list('KSA VAT Setting')
frappe.msgprint(_('Create <a href="{}">KSA VAT Setting</a> for this company').format(url))
return data
ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
# Sales Heading
append_data(data, 'VAT on Sales', '', '', '')
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
total_taxable_amount, total_taxable_adjustment_amount, \
total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice')
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
total_taxable_adjustment_amount, total_tax)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Sales Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
grand_total_taxable_adjustment_amount, grand_total_tax)
# Blank Line
append_data(data, '', '', '', '')
# Purchase Heading
append_data(data, 'VAT on Purchases', '', '', '')
grand_total_taxable_amount = 0
grand_total_taxable_adjustment_amount = 0
grand_total_tax = 0
for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
total_taxable_amount, total_taxable_adjustment_amount, \
total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice')
# Adding results to data
append_data(data, vat_setting.title, total_taxable_amount,
total_taxable_adjustment_amount, total_tax)
grand_total_taxable_amount += total_taxable_amount
grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
grand_total_tax += total_tax
# Purchase Grand Total
append_data(data, 'Grand Total', grand_total_taxable_amount,
grand_total_taxable_adjustment_amount, grand_total_tax)
return data
def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
'''
(KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
calculates and returns \n
total_taxable_amount, total_taxable_adjustment_amount, total_tax'''
from_date = filters.get('from_date')
to_date = filters.get('to_date')
# Initiate variables
total_taxable_amount = 0
total_taxable_adjustment_amount = 0
total_tax = 0
# Fetch All Invoices
invoices = frappe.get_list(doctype,
filters ={
'docstatus': 1,
'posting_date': ['between', [from_date, to_date]]
}, fields =['name', 'is_return'])
for invoice in invoices:
invoice_items = frappe.get_list(f'{doctype} Item',
filters ={
'docstatus': 1,
'parent': invoice.name,
'item_tax_template': vat_setting.item_tax_template
}, fields =['item_code', 'net_amount'])
for item in invoice_items:
# Summing up total taxable amount
if invoice.is_return == 0:
total_taxable_amount += item.net_amount
if invoice.is_return == 1:
total_taxable_adjustment_amount += item.net_amount
# Summing up total tax
total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
return total_taxable_amount, total_taxable_adjustment_amount, total_tax
def append_data(data, title, amount, adjustment_amount, vat_amount):
"""Returns data with appended value."""
data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
def get_tax_amount(item_code, account_head, doctype, parent):
if doctype == 'Sales Invoice':
tax_doctype = 'Sales Taxes and Charges'
elif doctype == 'Purchase Invoice':
tax_doctype = 'Purchase Taxes and Charges'
item_wise_tax_detail = frappe.get_value(tax_doctype, {
'docstatus': 1,
'parent': parent,
'account_head': account_head
}, 'item_wise_tax_detail')
tax_amount = 0
if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
for key, value in item_wise_tax_detail.items():
if key == item_code:
tax_amount = value[1]
break
return tax_amount

View File

@ -2,10 +2,36 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe
from erpnext.regional.united_arab_emirates.setup import make_custom_fields, add_print_formats from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def setup(company=None, patch=True): def setup(company=None, patch=True):
make_custom_fields() uae_custom_fields()
add_print_formats() add_print_formats()
add_permissions()
create_ksa_vat_setting(company)
make_qrcode_field()
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission('KSA VAT Setting', role, 0)
update_permission_property('KSA VAT Setting', role, 0, 'write', 1)
update_permission_property('KSA VAT Setting', role, 0, 'create', 1)
"""Enable KSA VAT Report"""
frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0)
def make_qrcode_field():
"""Created QR code Image file"""
qr_code_field = dict(
fieldname='qr_code',
label='QR Code',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1)
create_custom_field('Sales Invoice', qr_code_field)

View File

@ -0,0 +1,77 @@
import io
import os
import frappe
from pyqrcode import create as qr_create
from erpnext import get_region
def create_qr_code(doc, method):
"""Create QR Code after inserting Sales Inv
"""
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
# if QR Code field not present, do nothing
if not hasattr(doc, 'qr_code'):
return
# Don't create QR Code if it already exists
qr_code = doc.get("qr_code")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
meta = frappe.get_meta('Sales Invoice')
for field in meta.get_image_fields():
if field.fieldname == 'qr_code':
# Creating public url to print format
default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
# System Language
language = frappe.get_system_settings('language')
# creating qr code for the url
url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }"
qr_image = io.BytesIO()
url = qr_create(url, error='L')
url.png(qr_image, scale=2, quiet_zone=1)
# making file
filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc({
"doctype": "File",
"file_name": filename,
"is_private": 0,
"content": qr_image.getvalue(),
"attached_to_doctype": doc.get("doctype"),
"attached_to_name": doc.get("name"),
"attached_to_field": "qr_code"
})
_file.save()
# assigning to document
doc.db_set('qr_code', _file.file_url)
doc.notify_update()
break
def delete_qr_code_file(doc, method):
"""Delete QR Code on deleted sales invoice"""
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
if hasattr(doc, 'qr_code'):
if doc.get('qr_code'):
file_doc = frappe.get_list('File', {
'file_url': doc.get('qr_code')
})
if len(file_doc):
frappe.delete_doc('File', file_doc[0].name)

View File

@ -0,0 +1,47 @@
[
{
"type": "Sales Account",
"accounts": [
{
"title": "Standard rated Sales",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Zero rated domestic sales",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted sales",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Zero"
}
]
},
{
"type": "Purchase Account",
"accounts": [
{
"title": "Standard rated domestic purchases",
"item_tax_template": "KSA VAT 5%",
"account": "VAT 5%"
},
{
"title": "Imports subject to VAT paid at customs",
"item_tax_template": "KSA Excise 50%",
"account": "Excise 50%"
},
{
"title": "Zero rated purchases",
"item_tax_template": "KSA VAT Zero",
"account": "VAT Zero"
},
{
"title": "Exempted purchases",
"item_tax_template": "KSA VAT Exempted",
"account": "VAT Zero"
}
]
}
]

View File

@ -0,0 +1,46 @@
import json
import os
import frappe
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
def create_ksa_vat_setting(company):
"""On creation of first company. Creates KSA VAT Setting"""
company = frappe.get_doc('Company', company)
setup_taxes_and_charges(company.name, company.country)
file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json')
with open(file_path, 'r') as json_file:
account_data = json.load(json_file)
# Creating KSA VAT Setting
ksa_vat_setting = frappe.get_doc({
'doctype': 'KSA VAT Setting',
'company': company.name
})
for data in account_data:
if data['type'] == 'Sales Account':
for row in data['accounts']:
item_tax_template = row['item_tax_template']
account = row['account']
ksa_vat_setting.append('ksa_vat_sales_accounts', {
'title': row['title'],
'item_tax_template': f'{item_tax_template} - {company.abbr}',
'account': f'{account} - {company.abbr}'
})
elif data['type'] == 'Purchase Account':
for row in data['accounts']:
item_tax_template = row['item_tax_template']
account = row['account']
ksa_vat_setting.append('ksa_vat_purchase_accounts', {
'title': row['title'],
'item_tax_template': f'{item_tax_template} - {company.abbr}',
'account': f'{account} - {company.abbr}'
})
ksa_vat_setting.save()

View File

@ -1382,7 +1382,6 @@ def make_sales_order_workflow():
frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True) frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True)
frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True) frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True)
frappe.db.commit()
frappe.cache().hdel('roles', frappe.session.user) frappe.cache().hdel('roles', frappe.session.user)
workflow = frappe.get_doc({ workflow = frappe.get_doc({

View File

@ -44,7 +44,6 @@ class TestShoppingCartSettings(unittest.TestCase):
def test_tax_rule_validation(self): def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit()
cart_settings = self.get_cart_settings() cart_settings = self.get_cart_settings()
cart_settings.enabled = 1 cart_settings.enabled = 1

View File

@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe import frappe
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import ( from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
@ -18,10 +15,19 @@ def show_cart_count():
return False return False
def set_cart_count(login_manager): def set_cart_count(login_manager):
role, parties = check_customer_or_supplier() # since this is run only on hooks login event
if role == 'Supplier': return # make sure user is already a customer
# before trying to set cart count
user_is_customer = is_customer()
if not user_is_customer:
return
if show_cart_count(): if show_cart_count():
from erpnext.shopping_cart.cart import set_cart_count from erpnext.shopping_cart.cart import set_cart_count
# set_cart_count will try to fetch existing cart quotation
# or create one if non existent (and create a customer too)
# cart count is calculated from this quotation's items
set_cart_count() set_cart_count()
def clear_cart_count(login_manager): def clear_cart_count(login_manager):
@ -32,13 +38,13 @@ def update_website_context(context):
cart_enabled = is_cart_enabled() cart_enabled = is_cart_enabled()
context["shopping_cart_enabled"] = cart_enabled context["shopping_cart_enabled"] = cart_enabled
def check_customer_or_supplier(): def is_customer():
if frappe.session.user: if frappe.session.user and frappe.session.user != "Guest":
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user}) contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
if contact_name: if contact_name:
contact = frappe.get_doc('Contact', contact_name) contact = frappe.get_doc('Contact', contact_name)
for link in contact.links: for link in contact.links:
if link.link_doctype in ('Customer', 'Supplier'): if link.link_doctype == 'Customer':
return link.link_doctype, link.link_name return True
return 'Customer', None return False

View File

@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.exceptions import ValidationError from frappe.exceptions import ValidationError
@ -11,9 +8,10 @@ from frappe.utils import cint, flt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from erpnext.tests.utils import ERPNextTestCase
class TestBatch(unittest.TestCase): class TestBatch(ERPNextTestCase):
def test_item_has_batch_enabled(self): def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({ self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch", "doctype": "Batch",

View File

@ -14,51 +14,6 @@ class Bin(Document):
self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom') self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom')
self.set_projected_qty() self.set_projected_qty()
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.name,
"creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# update qty in future ale and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def update_qty(self, args):
# update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":
self.actual_qty = args.get("qty_after_transaction")
else:
self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty"))
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
self.set_projected_qty()
self.db_update()
def set_projected_qty(self): def set_projected_qty(self):
self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty) self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty)
+ flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty) + flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
@ -143,3 +98,67 @@ class Bin(Document):
def on_doctype_update(): def on_doctype_update():
frappe.db.add_index("Bin", ["item_code", "warehouse"]) frappe.db.add_index("Bin", ["item_code", "warehouse"])
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
update_qty(bin_name, args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": args.get('item_code'),
"warehouse": args.get('warehouse'),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get('name'),
"creation": args.get('creation')
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# update qty in future sle and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def get_bin_details(bin_name):
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production',
'reserved_qty_for_sub_contract'], as_dict=1)
def update_qty(bin_name, args):
bin_details = get_bin_details(bin_name)
# update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":
actual_qty = args.get('qty_after_transaction')
else:
actual_qty = bin_details.actual_qty + flt(args.get("actual_qty"))
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty"))
planned_qty = flt(bin_details.planned_qty) + flt(args.get("planned_qty"))
# compute projected qty
projected_qty = (flt(actual_qty) + flt(ordered_qty)
+ flt(indented_qty) + flt(planned_qty) - flt(reserved_qty)
- flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract))
frappe.db.set_value('Bin', bin_name, {
'actual_qty': actual_qty,
'ordered_qty': ordered_qty,
'reserved_qty': reserved_qty,
'indented_qty': indented_qty,
'planned_qty': planned_qty,
'projected_qty': projected_qty
})

View File

@ -5,7 +5,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
import unittest
import frappe import frappe
from frappe.utils import cstr, flt, nowdate, nowtime from frappe.utils import cstr, flt, nowdate, nowtime
@ -37,9 +36,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
) )
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.tests.utils import ERPNextTestCase
class TestDeliveryNote(unittest.TestCase): class TestDeliveryNote(ERPNextTestCase):
def test_over_billing_against_dn(self): def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)

View File

@ -14,11 +14,12 @@ from erpnext.stock.doctype.delivery_trip.delivery_trip import (
make_expense_claim, make_expense_claim,
notify_customers, notify_customers,
) )
from erpnext.tests.utils import create_test_contact_and_address from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
class TestDeliveryTrip(unittest.TestCase): class TestDeliveryTrip(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
driver = create_driver() driver = create_driver()
create_vehicle() create_vehicle()
create_delivery_notification() create_delivery_notification()
@ -32,6 +33,7 @@ class TestDeliveryTrip(unittest.TestCase):
frappe.db.sql("delete from `tabVehicle`") frappe.db.sql("delete from `tabVehicle`")
frappe.db.sql("delete from `tabEmail Template`") frappe.db.sql("delete from `tabEmail Template`")
frappe.db.sql("delete from `tabDelivery Trip`") frappe.db.sql("delete from `tabDelivery Trip`")
return super().tearDown()
def test_expense_claim_fields_are_fetched_properly(self): def test_expense_claim_fields_are_fetched_properly(self):
expense_claim = make_expense_claim(self.delivery_trip.name) expense_claim = make_expense_claim(self.delivery_trip.name)

View File

@ -4,7 +4,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
import unittest
import frappe import frappe
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
@ -25,7 +24,7 @@ from erpnext.stock.doctype.item.item import (
) )
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details from erpnext.stock.get_item_details import get_item_details
from erpnext.tests.utils import change_settings from erpnext.tests.utils import ERPNextTestCase, change_settings
test_ignore = ["BOM"] test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"] test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
@ -53,8 +52,9 @@ def make_item(item_code, properties=None):
return item return item
class TestItem(unittest.TestCase): class TestItem(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
frappe.flags.attribute_values = None frappe.flags.attribute_values = None
def get_item(self, idx): def get_item(self, idx):

View File

@ -4,7 +4,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import json import json
import unittest
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
@ -21,10 +20,12 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestItemAlternative(unittest.TestCase): class TestItemAlternative(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
make_items() make_items()
def test_alternative_item_for_subcontract_rm(self): def test_alternative_item_for_subcontract_rm(self):

View File

@ -3,17 +3,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
test_records = frappe.get_test_records('Item Attribute') test_records = frappe.get_test_records('Item Attribute')
from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError
from erpnext.tests.utils import ERPNextTestCase
class TestItemAttribute(unittest.TestCase): class TestItemAttribute(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
if frappe.db.exists("Item Attribute", "_Test_Length"): if frappe.db.exists("Item Attribute", "_Test_Length"):
frappe.delete_doc("Item Attribute", "_Test_Length") frappe.delete_doc("Item Attribute", "_Test_Length")

View File

@ -3,17 +3,17 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.test_runner import make_test_records_for_doctype from frappe.test_runner import make_test_records_for_doctype
from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem
from erpnext.stock.get_item_details import get_price_list_rate_for, process_args from erpnext.stock.get_item_details import get_price_list_rate_for, process_args
from erpnext.tests.utils import ERPNextTestCase
class TestItemPrice(unittest.TestCase): class TestItemPrice(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
frappe.db.sql("delete from `tabItem Price`") frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True) make_test_records_for_doctype("Item Price", force=True)

View File

@ -4,8 +4,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
@ -16,9 +14,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries, get_gl_entries,
make_purchase_receipt, make_purchase_receipt,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestLandedCostVoucher(unittest.TestCase): class TestLandedCostVoucher(ERPNextTestCase):
def test_landed_cost_voucher(self): def test_landed_cost_voucher(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)

View File

@ -6,8 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.utils import flt, today from frappe.utils import flt, today
@ -18,9 +16,10 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation, make_supplier_quotation,
raise_work_orders, raise_work_orders,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestMaterialRequest(unittest.TestCase): class TestMaterialRequest(ERPNextTestCase):
def test_make_purchase_order(self): def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert() mr = frappe.copy_doc(test_records[0]).insert()

View File

@ -6,6 +6,8 @@ from __future__ import unicode_literals
import unittest import unittest
# test_records = frappe.get_test_records('Packing Slip') # test_records = frappe.get_test_records('Packing Slip')
from erpnext.tests.utils import ERPNextTestCase
class TestPackingSlip(unittest.TestCase): class TestPackingSlip(unittest.TestCase):
pass pass

View File

@ -3,8 +3,6 @@
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch'] test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
@ -15,9 +13,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError, EmptyStockReconciliationItemsError,
) )
from erpnext.tests.utils import ERPNextTestCase
class TestPickList(unittest.TestCase): class TestPickList(ERPNextTestCase):
def test_pick_list_picks_warehouse_for_each_item(self): def test_pick_list_picks_warehouse_for_each_item(self):
try: try:

View File

@ -17,9 +17,10 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
from erpnext.tests.utils import ERPNextTestCase
class TestPurchaseReceipt(unittest.TestCase): class TestPurchaseReceipt(ERPNextTestCase):
def setUp(self): def setUp(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1) frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)

View File

@ -3,8 +3,6 @@
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from erpnext.stock.doctype.batch.test_batch import make_new_batch from erpnext.stock.doctype.batch.test_batch import make_new_batch
@ -13,9 +11,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.tests.utils import ERPNextTestCase
class TestPutawayRule(unittest.TestCase): class TestPutawayRule(ERPNextTestCase):
def setUp(self): def setUp(self):
if not frappe.db.exists("Item", "_Rice"): if not frappe.db.exists("Item", "_Rice"):
make_item("_Rice", { make_item("_Rice", {

View File

@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.utils import nowdate from frappe.utils import nowdate
@ -15,12 +13,14 @@ from erpnext.controllers.stock_controller import (
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestCase
# test_records = frappe.get_test_records('Quality Inspection') # test_records = frappe.get_test_records('Quality Inspection')
class TestQualityInspection(unittest.TestCase): class TestQualityInspection(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
create_item("_Test Item with QA") create_item("_Test Item with QA")
frappe.db.set_value( frappe.db.set_value(
"Item", "_Test Item with QA", "inspection_required_before_delivery", 1 "Item", "_Test Item with QA", "inspection_required_before_delivery", 1

View File

@ -6,8 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@ -20,9 +18,10 @@ test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No') test_records = frappe.get_test_records('Serial No')
from erpnext.stock.doctype.serial_no.serial_no import * from erpnext.stock.doctype.serial_no.serial_no import *
from erpnext.tests.utils import ERPNextTestCase
class TestSerialNo(unittest.TestCase): class TestSerialNo(ERPNextTestCase):
def test_cannot_create_direct(self): def test_cannot_create_direct(self):
frappe.delete_doc_if_exists("Serial No", "_TCSER0001") frappe.delete_doc_if_exists("Serial No", "_TCSER0001")

View File

@ -3,15 +3,15 @@
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
from datetime import date, timedelta from datetime import date, timedelta
import frappe import frappe
from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment
from erpnext.tests.utils import ERPNextTestCase
class TestShipment(unittest.TestCase): class TestShipment(ERPNextTestCase):
def test_shipment_from_delivery_note(self): def test_shipment_from_delivery_note(self):
delivery_note = create_test_delivery_note() delivery_note = create_test_delivery_note()
delivery_note.submit() delivery_note.submit()
@ -47,7 +47,6 @@ def create_test_delivery_note():
} }
) )
delivery_note.insert() delivery_note.insert()
frappe.db.commit()
return delivery_note return delivery_note
@ -91,7 +90,6 @@ def create_test_shipment(delivery_notes = None):
} }
) )
shipment.insert() shipment.insert()
frappe.db.commit()
return shipment return shipment

View File

@ -317,7 +317,7 @@
"in_create": 1, "in_create": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-10-08 12:42:51.857631", "modified": "2021-10-08 13:42:51.857631",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Ledger Entry", "name": "Stock Ledger Entry",

View File

@ -181,3 +181,4 @@ def on_doctype_update():
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"]) frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"]) frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")

View File

@ -3,8 +3,6 @@
# See license.txt # See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today from frappe.utils import add_days, today
@ -21,9 +19,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.tests.utils import ERPNextTestCase
class TestStockLedgerEntry(unittest.TestCase): class TestStockLedgerEntry(ERPNextTestCase):
def setUp(self): def setUp(self):
items = create_items() items = create_items()
reset('Stock Entry') reset('Stock Entry')

View File

@ -6,8 +6,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.utils import add_days, flt, nowdate, nowtime, random_string from frappe.utils import add_days, flt, nowdate, nowtime, random_string
@ -22,12 +20,13 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.tests.utils import change_settings from erpnext.tests.utils import ERPNextTestCase, change_settings
class TestStockReconciliation(unittest.TestCase): class TestStockReconciliation(ERPNextTestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(self):
super().setUpClass()
create_batch_or_serial_no_items() create_batch_or_serial_no_items()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@ -372,7 +371,6 @@ class TestStockReconciliation(unittest.TestCase):
""" """
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.stock_ledger import NegativeStockError from erpnext.stock.stock_ledger import NegativeStockError
frappe.db.commit()
item_code = "Backdated-Reco-Cancellation-Item" item_code = "Backdated-Reco-Cancellation-Item"
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
@ -395,10 +393,6 @@ class TestStockReconciliation(unittest.TestCase):
repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name})) repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name}))
self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation") self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation")
# teardown
frappe.db.rollback()
def test_valid_batch(self): def test_valid_batch(self):
create_batch_item_with_batch("Testing Batch Item 1", "001") create_batch_item_with_batch("Testing Batch Item 1", "001")
create_batch_item_with_batch("Testing Batch Item 2", "002") create_batch_item_with_batch("Testing Batch Item 2", "002")

View File

@ -7,9 +7,12 @@ import unittest
import frappe import frappe
from erpnext.tests.utils import ERPNextTestCase
class TestStockSettings(unittest.TestCase):
class TestStockSettings(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
frappe.db.set_value("Stock Settings", None, "clean_description_html", 0) frappe.db.set_value("Stock Settings", None, "clean_description_html", 0)
def test_settings(self): def test_settings(self):

View File

@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import unittest
import frappe import frappe
from frappe.test_runner import make_test_records from frappe.test_runner import make_test_records
from frappe.utils import cint from frappe.utils import cint
@ -12,11 +10,13 @@ import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('Warehouse') test_records = frappe.get_test_records('Warehouse')
class TestWarehouse(unittest.TestCase): class TestWarehouse(ERPNextTestCase):
def setUp(self): def setUp(self):
super().setUp()
if not frappe.get_value('Item', '_Test Item'): if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item') make_test_records('Item')

View File

@ -0,0 +1,97 @@
{
"creation": "2021-07-29 12:32:08.929900",
"docstatus": 0,
"doctype": "Form Tour",
"idx": 0,
"is_standard": 1,
"modified": "2021-10-05 13:11:13.119453",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
"owner": "Administrator",
"reference_doctype": "Material Request",
"save_on_complete": 1,
"steps": [
{
"description": "The purpose of the material request can be selected here. For now select \"Purchase\" as the purpose.",
"field": "",
"fieldname": "material_request_type",
"fieldtype": "Select",
"has_next_condition": 1,
"is_table_field": 0,
"label": "Purpose",
"next_step_condition": "eval: doc.material_request_type == \"Purchase\"",
"parent_field": "",
"position": "Bottom",
"title": "Purpose"
},
{
"description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
"field": "",
"fieldname": "schedule_date",
"fieldtype": "Date",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Required By",
"next_step_condition": "",
"parent_field": "",
"position": "Left",
"title": "Required By"
},
{
"description": "Setting the target warehouse sets it for all the items.",
"field": "",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Set Target Warehouse",
"next_step_condition": "",
"parent_field": "",
"position": "Left",
"title": "Target Warehouse"
},
{
"description": "Items table",
"field": "",
"fieldname": "items",
"fieldtype": "Table",
"has_next_condition": 0,
"is_table_field": 0,
"label": "Items",
"parent_field": "",
"position": "Bottom",
"title": "Items"
},
{
"child_doctype": "Material Request Item",
"description": "Select an Item code. Item details will be fetched automatically.",
"field": "",
"fieldname": "item_code",
"fieldtype": "Link",
"has_next_condition": 1,
"is_table_field": 1,
"label": "Item Code",
"next_step_condition": "eval: doc.item_code",
"parent_field": "",
"parent_fieldname": "items",
"position": "Right",
"title": "Item Code"
},
{
"child_doctype": "Material Request Item",
"description": "Enter the required quantity for the material.",
"field": "",
"fieldname": "qty",
"fieldtype": "Float",
"has_next_condition": 0,
"is_table_field": 1,
"label": "Quantity",
"parent_field": "",
"parent_fieldname": "items",
"position": "Bottom",
"title": "Quantity"
}
],
"title": "Material Request"
}

View File

@ -5,9 +5,10 @@ from frappe import _dict
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
from erpnext.tests.utils import ERPNextTestCase
class TestStockAnalyticsReport(unittest.TestCase): class TestStockAnalyticsReport(ERPNextTestCase):
def test_get_period_date_ranges(self): def test_get_period_date_ranges(self):
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06") filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")

View File

@ -13,8 +13,8 @@ from six import iteritems
import erpnext import erpnext
from erpnext.stock.utils import ( from erpnext.stock.utils import (
get_bin,
get_incoming_outgoing_rate_for_cancel, get_incoming_outgoing_rate_for_cancel,
get_or_make_bin,
get_valuation_method, get_valuation_method,
) )
@ -805,14 +805,13 @@ class update_entries_after(object):
def update_bin(self): def update_bin(self):
# update bin for each warehouse # update bin for each warehouse
for warehouse, data in iteritems(self.data): for warehouse, data in iteritems(self.data):
bin_doc = get_bin(self.item_code, warehouse) bin_record = get_or_make_bin(self.item_code, warehouse)
bin_doc.update({
frappe.db.set_value('Bin', bin_record, {
"valuation_rate": data.valuation_rate, "valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction, "actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value "stock_value": data.stock_value
}) })
bin_doc.flags.via_stock_ledger_entry = True
bin_doc.save(ignore_permissions=True)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
@ -918,7 +917,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
company = erpnext.get_default_company() company = erpnext.get_default_company()
last_valuation_rate = frappe.db.sql("""select valuation_rate last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` from `tabStock Ledger Entry` force index (item_warehouse)
where where
item_code = %s item_code = %s
AND warehouse = %s AND warehouse = %s
@ -929,7 +928,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not last_valuation_rate: if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse # Get valuation rate from last sle for the item against any warehouse
last_valuation_rate = frappe.db.sql("""select valuation_rate last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` from `tabStock Ledger Entry` force index (item_code)
where where
item_code = %s item_code = %s
AND valuation_rate > 0 AND valuation_rate > 0

View File

@ -180,12 +180,27 @@ def get_bin(item_code, warehouse):
bin_obj.flags.ignore_permissions = True bin_obj.flags.ignore_permissions = True
return bin_obj return bin_obj
def get_or_make_bin(item_code, warehouse) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record:
bin_obj = frappe.get_doc({
"doctype": "Bin",
"item_code": item_code,
"warehouse": warehouse,
})
bin_obj.flags.ignore_permissions = 1
bin_obj.insert()
bin_record = bin_obj.name
return bin_record
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item: if is_stock_item:
bin = get_bin(args.get("item_code"), args.get("warehouse")) bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher) update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
return bin
else: else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))

View File

@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy import copy
import unittest
from contextlib import contextmanager from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional from typing import Any, Dict, NewType, Optional
@ -12,6 +13,21 @@ ReportFilters = Dict[str, Any]
ReportName = NewType("ReportName", str) ReportName = NewType("ReportName", str)
class ERPNextTestCase(unittest.TestCase):
"""A sane default test class for ERPNext tests."""
@classmethod
def setUpClass(cls) -> None:
frappe.db.commit()
return super().setUpClass()
@classmethod
def tearDownClass(cls) -> None:
frappe.db.rollback()
return super().tearDownClass()
def create_test_contact_and_address(): def create_test_contact_and_address():
frappe.db.sql('delete from tabContact') frappe.db.sql('delete from tabContact')
frappe.db.sql('delete from `tabContact Email`') frappe.db.sql('delete from `tabContact Email`')