Merge branch 'develop' into pos-service-items

This commit is contained in:
Saqib Ansari 2022-02-01 12:25:25 +05:30 committed by GitHub
commit 4557b6a150
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
579 changed files with 10016 additions and 27291 deletions

View File

@ -28,6 +28,7 @@ ignore =
B007,
B950,
W191,
E124, # closing bracket, irritating while writing QB code
max-line-length = 200
exclude=.github/helper/semgrep_rules

View File

@ -40,6 +40,7 @@ body:
- HR
- projects
- support
- CRM
- assets
- integrations
- quality
@ -48,6 +49,7 @@ body:
- agriculture
- education
- non-profit
- other
validations:
required: true

View File

@ -12,17 +12,30 @@ git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
mkdir ~/frappe-bench/sites/test_site
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
if [ "$DB" == "mariadb" ];then
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_mariadb.json" ~/frappe-bench/sites/test_site/site_config.json
else
cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config_postgres.json" ~/frappe-bench/sites/test_site/site_config.json
fi
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
if [ "$DB" == "mariadb" ];then
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
fi
if [ "$DB" == "postgres" ];then
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE DATABASE test_frappe" -U postgres;
echo "travis" | psql -h 127.0.0.1 -p 5432 -c "CREATE USER test_frappe WITH PASSWORD 'test_frappe'" -U postgres;
fi
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp

View File

@ -13,4 +13,4 @@
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"],
"throttle_user_limit": 100
}
}

View File

@ -0,0 +1,18 @@
{
"db_host": "127.0.0.1",
"db_port": 5432,
"db_name": "test_frappe",
"db_password": "test_frappe",
"db_type": "postgres",
"allow_tests": true,
"auto_email_id": "test@example.com",
"mail_server": "smtp.example.com",
"mail_login": "test@example.com",
"mail_password": "test",
"admin_password": "admin",
"root_login": "postgres",
"root_password": "travis",
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"],
"throttle_user_limit": 100
}

55
.github/labeler.yml vendored Normal file
View File

@ -0,0 +1,55 @@
accounts:
- erpnext/accounts/*
- erpnext/controllers/accounts_controller.py
- erpnext/controllers/taxes_and_totals.py
stock:
- erpnext/stock/*
- erpnext/controllers/stock_controller.py
- erpnext/controllers/item_variant.py
assets:
- erpnext/assets/*
regional:
- erpnext/regional/*
selling:
- erpnext/selling/*
- erpnext/controllers/selling_controller.py
buying:
- erpnext/buying/*
- erpnext/controllers/buying_controller.py
support:
- erpnext/support/*
POS:
- pos*
ecommerce:
- erpnext/e_commerce/*
maintenance:
- erpnext/maintenance/*
manufacturing:
- erpnext/manufacturing/*
crm:
- erpnext/crm/*
HR:
- erpnext/hr/*
payroll:
- erpnext/payroll*
projects:
- erpnext/projects/*
# Any python files modifed but no test files modified
needs-tests:
- any: ['erpnext/**/*.py']
all: ['!erpnext/**/test*.py']

View File

@ -12,7 +12,7 @@ jobs:
- name: 'Setup Environment'
uses: actions/setup-python@v2
with:
python-version: 3.6
python-version: 3.8
- name: 'Clone repo'
uses: actions/checkout@v2

12
.github/workflows/labeller.yml vendored Normal file
View File

@ -0,0 +1,12 @@
name: "Pull Request Labeler"
on:
pull_request_target:
types: [opened, reopened]
jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v3
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

View File

@ -34,7 +34,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
@ -80,6 +80,9 @@ jobs:
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Patch Tests
run: |

View File

@ -1,10 +1,11 @@
name: Server
name: Server (Mariadb)
on:
pull_request:
paths-ignore:
- '**.js'
- '**.md'
- '**.html'
workflow_dispatch:
push:
branches: [ develop ]
@ -13,7 +14,7 @@ on:
- '**.md'
concurrency:
group: server-develop-${{ github.event.number }}
group: server-mariadb-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
@ -45,7 +46,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
@ -92,6 +93,7 @@ jobs:
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: server
- name: Run Tests

View File

@ -0,0 +1,105 @@
name: Server (Postgres)
on:
pull_request:
paths-ignore:
- '**.js'
- '**.md'
- '**.html'
types: [opened, labelled, synchronize, reopened]
concurrency:
group: server-postgres-develop-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
if: ${{ contains(github.event.pull_request.labels.*.name, 'postgres') }}
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
container: [1, 2, 3]
name: Python Unit Tests
services:
postgres:
image: postgres:13.3
env:
POSTGRES_PASSWORD: travis
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Setup Node
uses: actions/setup-node@v2
with:
node-version: 14
check-latest: true
- name: Add to Hosts
run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: postgres
TYPE: server
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io

View File

@ -36,7 +36,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
python-version: 3.8
- uses: actions/setup-node@v2
with:

View File

@ -23,13 +23,13 @@ erpnext/stock/ @marination @rohitwaghchaure @ankush
erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal @pateljannat
erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
erpnext/public/ @nextchamp-saqib @marination
.github/ @surajshetty3416 @ankush
.github/ @ankush
requirements.txt @gavindsouza

View File

@ -254,11 +254,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return
account_currency = get_account_currency(item.expense_account)
account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
@ -279,6 +281,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount:
return
# check if books nor frozen till endate:
if getdate(end_date) >= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@ -406,8 +412,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': credit_account,
'credit': base_amount,
'credit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,
@ -420,8 +424,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': debit_account,
'debit': base_amount,
'debit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,

View File

@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
frm.trigger('add_toolbar_buttons');
}
if (frm.has_perm('write')) {
frm.add_custom_button(__('Update Account Name / Number'), function () {
frm.trigger("update_account_number");
});
frm.add_custom_button(__('Merge Account'), function () {
frm.trigger("merge_account");
});
}, __('Actions'));
frm.add_custom_button(__('Update Account Name / Number'), function () {
frm.trigger("update_account_number");
}, __('Actions'));
}
}
},
@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
}
},
add_toolbar_buttons: function(frm) {
frm.add_custom_button(__('Chart of Accounts'),
function () { frappe.set_route("Tree", "Account"); });
frm.add_custom_button(__('Chart of Accounts'), () => {
frappe.set_route("Tree", "Account");
}, __('View'));
if (frm.doc.is_group == 1) {
frm.add_custom_button(__('Group to Non-Group'), function () {
frm.add_custom_button(__('Convert to Non-Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_group_to_ledger',
@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
});
}, __('Actions'));
} else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
frm.add_custom_button(__('Ledger'), function () {
frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date,
@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
});
}, __('View'));
frm.add_custom_button(__('Non-Group to Group'), function () {
frm.add_custom_button(__('Convert to Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_ledger_to_group',
@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
});
}, __('Actions'));
}
},

View File

@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.set_query("bank_account", function () {
return {
filters: {
company: ["in", frm.doc.company],
company: frm.doc.company,
'is_company_account': 1
},
};

View File

@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal",
"description",
"reference_number",
"bank_account"
"bank_account",
"currency"
],
},
});

View File

@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import ILLEGAL_CHARACTERS_RE, handle_html
from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
INVALID_VALUES = ("", None)
class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs):
@ -95,6 +96,18 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
def parse_data_from_template(raw_data):
data = []
for i, row in enumerate(raw_data):
if all(v in INVALID_VALUES for v in row):
# empty row
continue
data.append(row)
return data
def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
"""This method runs in background job"""
@ -104,7 +117,8 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
data = import_file.raw_data
data = parse_data_from_template(import_file.raw_data)
if import_file_path:
add_bank_account(data, bank_account)

View File

@ -2,9 +2,10 @@
# For license information, please see license.txt
from functools import reduce
import frappe
from frappe.utils import flt
from six.moves import reduce
from erpnext.controllers.status_updater import StatusUpdater

View File

@ -15,17 +15,6 @@ frappe.ui.form.on('Cost Center', {
}
}
});
frm.set_query("cost_center", "distributed_cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
enable_distributed_cost_center: 0,
name: ['!=', frm.doc.name]
}
};
});
},
refresh: function(frm) {
if (!frm.is_new()) {

View File

@ -16,9 +16,6 @@
"cb0",
"is_group",
"disabled",
"section_break_9",
"enable_distributed_cost_center",
"distributed_cost_center",
"lft",
"rgt",
"old_parent"
@ -122,31 +119,13 @@
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"default": "0",
"fieldname": "enable_distributed_cost_center",
"fieldtype": "Check",
"label": "Enable Distributed Cost Center"
},
{
"depends_on": "eval:doc.is_group==0",
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"depends_on": "enable_distributed_cost_center",
"fieldname": "distributed_cost_center",
"fieldtype": "Table",
"label": "Distributed Cost Center",
"options": "Distributed Cost Center"
}
],
"icon": "fa fa-money",
"idx": 1,
"is_tree": 1,
"links": [],
"modified": "2020-06-17 16:09:30.025214",
"modified": "2022-01-31 13:22:58.916273",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center",
@ -189,5 +168,6 @@
"search_fields": "parent_cost_center, is_group",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@ -4,7 +4,6 @@
import frappe
from frappe import _
from frappe.utils import cint
from frappe.utils.nestedset import NestedSet
from erpnext.accounts.utils import validate_field_number
@ -20,24 +19,6 @@ class CostCenter(NestedSet):
def validate(self):
self.validate_mandatory()
self.validate_parent_cost_center()
self.validate_distributed_cost_center()
def validate_distributed_cost_center(self):
if cint(self.enable_distributed_cost_center):
if not self.distributed_cost_center:
frappe.throw(_("Please enter distributed cost center"))
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
if not self.get('__islocal'):
if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
and self.check_if_part_of_distributed_cost_center():
frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
else:
self.distributed_cost_center = []
def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center:
@ -64,10 +45,10 @@ class CostCenter(NestedSet):
@frappe.whitelist()
def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center):
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
if self.check_if_part_of_distributed_cost_center():
frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
if self.if_allocation_exists_against_cost_center():
frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
if self.check_if_part_of_cost_center_allocation():
frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1
@ -81,8 +62,17 @@ class CostCenter(NestedSet):
return frappe.db.sql("select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2", self.name)
def check_if_part_of_distributed_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
def if_allocation_exists_against_cost_center(self):
return frappe.db.get_value("Cost Center Allocation", filters = {
"main_cost_center": self.name,
"docstatus": 1
})
def check_if_part_of_cost_center_allocation(self):
return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
"cost_center": self.name,
"docstatus": 1
})
def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided
@ -126,8 +116,4 @@ def on_doctype_update():
def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account
return new_account
def check_if_distributed_cost_center_enabled(cost_center_list):
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
return next((True for x in value_list if x[0]), False)
return new_account

View File

@ -23,33 +23,6 @@ class TestCostCenter(unittest.TestCase):
self.assertRaises(frappe.ValidationError, cost_center.save)
def test_validate_distributed_cost_center(self):
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
frappe.get_doc(test_records[0]).insert()
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
frappe.get_doc(test_records[1]).insert()
invalid_distributed_cost_center = frappe.get_doc({
"company": "_Test Company",
"cost_center_name": "_Test Distributed Cost Center",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC",
"enable_distributed_cost_center": 1,
"distributed_cost_center": [{
"cost_center": "_Test Cost Center - _TC",
"percentage_allocation": 40
}, {
"cost_center": "_Test Cost Center 2 - _TC",
"percentage_allocation": 50
}
]
})
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
def create_cost_center(**args):
args = frappe._dict(args)
if args.cost_center_name:

View File

@ -0,0 +1,19 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cost Center Allocation', {
setup: function(frm) {
let filters = {"is_group": 0};
if (frm.doc.company) {
$.extend(filters, {
"company": frm.doc.company
});
}
frm.set_query('main_cost_center', function() {
return {
filters: filters
};
});
}
});

View File

@ -0,0 +1,128 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "CC-ALLOC-.#####",
"creation": "2022-01-13 20:07:29.871109",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"main_cost_center",
"company",
"column_break_2",
"valid_from",
"section_break_5",
"allocation_percentages",
"amended_from"
],
"fields": [
{
"fieldname": "main_cost_center",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Main Cost Center",
"options": "Cost Center",
"reqd": 1
},
{
"default": "Today",
"fieldname": "valid_from",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid From",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fetch_from": "main_cost_center.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "allocation_percentages",
"fieldtype": "Table",
"label": "Cost Center Allocation Percentages",
"options": "Cost Center Allocation Percentage",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Cost Center Allocation",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-01-31 11:47:12.086253",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center Allocation",
"name_case": "UPPER CASE",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,90 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_days, format_date, getdate
class MainCostCenterCantBeChild(frappe.ValidationError):
pass
class InvalidMainCostCenter(frappe.ValidationError):
pass
class InvalidChildCostCenter(frappe.ValidationError):
pass
class WrongPercentageAllocation(frappe.ValidationError):
pass
class InvalidDateError(frappe.ValidationError):
pass
class CostCenterAllocation(Document):
def validate(self):
self.validate_total_allocation_percentage()
self.validate_from_date_based_on_existing_gle()
self.validate_backdated_allocation()
self.validate_main_cost_center()
self.validate_child_cost_centers()
def validate_total_allocation_percentage(self):
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
if total_percentage != 100:
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
def validate_from_date_based_on_existing_gle(self):
# Check if GLE exists against the main cost center
# If exists ensure from date is set after posting date of last GLE
last_gle_date = frappe.db.get_value("GL Entry",
{"cost_center": self.main_cost_center, "is_cancelled": 0},
"posting_date", order_by="posting_date desc")
if last_gle_date:
if getdate(self.valid_from) <= getdate(last_gle_date):
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
.format(last_gle_date, self.main_cost_center), InvalidDateError)
def validate_backdated_allocation(self):
# Check if there are any future existing allocation records against the main cost center
# If exists, warn the user about it
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
"main_cost_center": self.main_cost_center,
"valid_from": (">=", self.valid_from),
"name": ("!=", self.name),
"docstatus": 1
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
if future_allocation:
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
title=_("Warning!"), indicator="orange", alert=1
)
def validate_main_cost_center(self):
# Main cost center itself cannot be entered in child table
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
.format(self.main_cost_center), MainCostCenterCantBeChild)
# If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
"cost_center": self.main_cost_center,
"docstatus": 1
}, fieldname='parent')
if parent:
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
.format(self.main_cost_center, parent), InvalidMainCostCenter)
def validate_child_cost_centers(self):
# Check if child cost center is used as main cost center in any existing allocation
main_cost_centers = [d.main_cost_center for d in
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
for d in self.allocation_percentages:
if d.cost_center in main_cost_centers:
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
.format(d.cost_center), InvalidChildCostCenter)

View File

@ -0,0 +1,156 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (
InvalidChildCostCenter,
InvalidDateError,
InvalidMainCostCenter,
MainCostCenterCantBeChild,
WrongPercentageAllocation,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestCostCenterAllocation(unittest.TestCase):
def setUp(self):
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company")
def test_gle_based_on_cost_center_allocation(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
cost_center = "Main Cost Center 1 - _TC", submit=True)
expected_values = [
["Sub Cost Center 1 - _TC", 0.0, 60],
["Sub Cost Center 2 - _TC", 0.0, 40]
]
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.cost_center, gle.debit, gle.credit)
.where(gle.voucher_type == 'Journal Entry')
.where(gle.voucher_no == jv.name)
.where(gle.account == 'Sales - _TC')
.orderby(gle.cost_center)
).run(as_dict=1)
self.assertTrue(gl_entries)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.cost_center)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
cca.cancel()
jv.cancel()
def test_main_cost_center_cant_be_child(self):
# Main cost center itself cannot be entered in child table
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Main Cost Center 1 - _TC": 40
}, save=False
)
self.assertRaises(MainCostCenterCantBeChild, cca.save)
def test_invalid_main_cost_center(self):
# If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
{
"Sub Cost Center 2 - _TC": 100
}, save=False
)
self.assertRaises(InvalidMainCostCenter, cca2.save)
cca1.cancel()
def test_if_child_cost_center_has_any_allocation_record(self):
# Check if any child cost center is used as main cost center in any other existing allocation
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
{
"Main Cost Center 1 - _TC": 60,
"Sub Cost Center 1 - _TC": 40
}, save=False
)
self.assertRaises(InvalidChildCostCenter, cca2.save)
cca1.cancel()
def test_total_percentage(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 40,
"Sub Cost Center 2 - _TC": 40
}, save=False
)
self.assertRaises(WrongPercentageAllocation, cca.save)
def test_valid_from_based_on_existing_gle(self):
# GLE posted against Sub Cost Center 1 on today
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
# try to set valid from as yesterday
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}, valid_from=add_days(today(), -1), save=False
)
self.assertRaises(InvalidDateError, cca.save)
jv.cancel()
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
valid_from=None, valid_upto=None, save=True, submit=True):
doc = frappe.new_doc("Cost Center Allocation")
doc.main_cost_center = main_cost_center
doc.company = company
doc.valid_from = valid_from or today()
doc.valid_upto = valid_upto
for cc, percentage in allocation_percentages.items():
doc.append("allocation_percentages", {
"cost_center": cc,
"percentage": percentage
})
if save:
doc.save()
if submit:
doc.submit()
return doc

View File

@ -1,12 +1,13 @@
{
"actions": [],
"creation": "2020-03-19 12:34:01.500390",
"allow_rename": 1,
"creation": "2022-01-03 18:10:11.697198",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"cost_center",
"percentage_allocation"
"percentage"
],
"fields": [
{
@ -18,23 +19,23 @@
"reqd": 1
},
{
"fieldname": "percentage_allocation",
"fieldtype": "Float",
"fieldname": "percentage",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Percentage Allocation",
"label": "Percentage (%)",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-03-19 12:54:43.674655",
"modified": "2022-01-03 18:10:20.029821",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Distributed Cost Center",
"name": "Cost Center Allocation Percentage",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CostCenterAllocationPercentage(Document):
pass

View File

@ -0,0 +1,45 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Currency Exchange Settings', {
service_provider: function(frm) {
if (frm.doc.service_provider == "exchangerate.host") {
let result = ['result'];
let params = {
date: '{transaction_date}',
from: '{from_currency}',
to: '{to_currency}'
};
add_param(frm, "https://api.exchangerate.host/convert", params, result);
} else if (frm.doc.service_provider == "frankfurter.app") {
let result = ['rates', '{to_currency}'];
let params = {
base: '{from_currency}',
symbols: '{to_currency}'
};
add_param(frm, "https://frankfurter.app/{transaction_date}", params, result);
}
}
});
function add_param(frm, api, params, result) {
var row;
frm.clear_table("req_params");
frm.clear_table("result_key");
frm.doc.api_endpoint = api;
$.each(params, function(key, value) {
row = frm.add_child("req_params");
row.key = key;
row.value = value;
});
$.each(result, function(key, value) {
row = frm.add_child("result_key");
row.key = value;
});
frm.refresh_fields();
}

View File

@ -0,0 +1,126 @@
{
"actions": [],
"creation": "2022-01-10 13:03:26.237081",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"api_details_section",
"service_provider",
"api_endpoint",
"url",
"column_break_3",
"help",
"section_break_2",
"req_params",
"column_break_4",
"result_key"
],
"fields": [
{
"fieldname": "api_details_section",
"fieldtype": "Section Break",
"label": "API Details"
},
{
"fieldname": "api_endpoint",
"fieldtype": "Data",
"in_list_view": 1,
"label": "API Endpoint",
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
"reqd": 1
},
{
"fieldname": "url",
"fieldtype": "Data",
"label": "Example URL",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "help",
"fieldtype": "HTML",
"label": "Help",
"options": "<h3>Currency Exchange Settings Help</h3>\n<p>There are 3 variables that could be used within the endpoint, result key and in values of the parameter.</p>\n<p>Exchange rate between {from_currency} and {to_currency} on {transaction_date} is fetched by the API.</p>\n<p>Example: If your endpoint is exchange.com/2021-08-01, then, you will have to input exchange.com/{transaction_date}</p>"
},
{
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Request Parameters"
},
{
"fieldname": "req_params",
"fieldtype": "Table",
"label": "Parameters",
"options": "Currency Exchange Settings Details",
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
"reqd": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "result_key",
"fieldtype": "Table",
"label": "Result Key",
"options": "Currency Exchange Settings Result",
"read_only_depends_on": "eval: doc.service_provider != \"Custom\"",
"reqd": 1
},
{
"fieldname": "service_provider",
"fieldtype": "Select",
"label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-01-10 15:51:14.521174",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Accounts User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -0,0 +1,82 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import requests
from frappe import _
from frappe.model.document import Document
from frappe.utils import nowdate
class CurrencyExchangeSettings(Document):
def validate(self):
self.set_parameters_and_result()
response, value = self.validate_parameters()
self.validate_result(response, value)
def set_parameters_and_result(self):
if self.service_provider == 'exchangerate.host':
self.set('result_key', [])
self.set('req_params', [])
self.api_endpoint = "https://api.exchangerate.host/convert"
self.append('result_key', {'key': 'result'})
self.append('req_params', {'key': 'date', 'value': '{transaction_date}'})
self.append('req_params', {'key': 'from', 'value': '{from_currency}'})
self.append('req_params', {'key': 'to', 'value': '{to_currency}'})
elif self.service_provider == 'frankfurter.app':
self.set('result_key', [])
self.set('req_params', [])
self.api_endpoint = "https://frankfurter.app/{transaction_date}"
self.append('result_key', {'key': 'rates'})
self.append('result_key', {'key': '{to_currency}'})
self.append('req_params', {'key': 'base', 'value': '{from_currency}'})
self.append('req_params', {'key': 'symbols', 'value': '{to_currency}'})
def validate_parameters(self):
if frappe.flags.in_test:
return None, None
params = {}
for row in self.req_params:
params[row.key] = row.value.format(
transaction_date=nowdate(),
to_currency='INR',
from_currency='USD'
)
api_url = self.api_endpoint.format(
transaction_date=nowdate(),
to_currency='INR',
from_currency='USD'
)
try:
response = requests.get(api_url, params=params)
except requests.exceptions.RequestException as e:
frappe.throw("Error: " + str(e))
response.raise_for_status()
value = response.json()
return response, value
def validate_result(self, response, value):
if frappe.flags.in_test:
return
try:
for key in self.result_key:
value = value[str(key.key).format(
transaction_date=nowdate(),
to_currency='INR',
from_currency='USD'
)]
except Exception:
frappe.throw("Invalid result key. Response: " + response.text)
if not isinstance(value, (int, float)):
frappe.throw(_("Returned exchange rate is neither integer not float."))
self.url = response.url
frappe.msgprint("Exchange rate of USD to INR is " + str(value))

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
import unittest
class TestCurrencyExchangeSettings(unittest.TestCase):
pass

View File

@ -0,0 +1,39 @@
{
"actions": [],
"creation": "2021-09-02 14:54:49.033512",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"key",
"value"
],
"fields": [
{
"fieldname": "key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Key",
"reqd": 1
},
{
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Value",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-11-03 19:14:55.889037",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings Details",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CurrencyExchangeSettingsDetails(Document):
pass

View File

@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-09-03 13:17:22.088259",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"key"
],
"fields": [
{
"fieldname": "key",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Key",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-11-03 19:14:40.054245",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Currency Exchange Settings Result",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CurrencyExchangeSettingsResult(Document):
pass

View File

@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", {
if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm);
}, __('Make'));
}, __('Actions'));
}
if (frm.doc.__islocal) {

View File

@ -13,6 +13,7 @@
"voucher_type",
"naming_series",
"finance_book",
"reversal_of",
"tax_withholding_category",
"column_break1",
"from_template",
@ -515,13 +516,21 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
},
{
"depends_on": "eval:doc.docstatus",
"fieldname": "reversal_of",
"fieldtype": "Link",
"label": "Reversal Of",
"options": "Journal Entry",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2021-09-09 15:31:14.484029",
"modified": "2022-01-04 13:39:36.485954",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@ -407,13 +407,14 @@ class JournalEntry(AccountsController):
debit_or_credit = 'Debit' if d.debit else 'Credit'
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
debit_or_credit)
against_voucher = ['', against_voucher[1]]
else:
if d.reference_type == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
else:
party_account = against_voucher[1]
if (against_voucher[0] != d.party or party_account != d.account):
if (against_voucher[0] != cstr(d.party) or party_account != d.account):
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
d.reference_type, d.reference_name))
@ -478,13 +479,22 @@ class JournalEntry(AccountsController):
def set_against_account(self):
accounts_debited, accounts_credited = [], []
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
for d in self.get('accounts'):
if d.reference_type == 'Sales Invoice':
field = 'customer'
else:
field = 'supplier'
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get('accounts'):
@ -1157,9 +1167,8 @@ def make_inter_company_journal_entry(name, voucher_type, company):
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
target.reference_type = "Journal Entry"
target.reference_name = source_parent.name
def post_process(source, target):
target.reversal_of = source.name
doclist = get_mapped_doc("Journal Entry", source_name, {
"Journal Entry": {
@ -1177,9 +1186,8 @@ def make_reverse_journal_entry(source_name, target_doc=None):
"debit": "credit",
"credit_in_account_currency": "debit_in_account_currency",
"credit": "debit",
},
"postprocess": update_accounts,
}
},
}, target_doc)
}, target_doc, post_process)
return doclist

View File

@ -0,0 +1,128 @@
// Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Ledger Merge', {
setup: function(frm) {
frappe.realtime.on('ledger_merge_refresh', ({ ledger_merge }) => {
if (ledger_merge !== frm.doc.name) return;
frappe.model.clear_doc(frm.doc.doctype, frm.doc.name);
frappe.model.with_doc(frm.doc.doctype, frm.doc.name).then(() => {
frm.refresh();
});
});
frappe.realtime.on('ledger_merge_progress', data => {
if (data.ledger_merge !== frm.doc.name) return;
let message = __('Merging {0} of {1}', [data.current, data.total]);
let percent = Math.floor((data.current * 100) / data.total);
frm.dashboard.show_progress(__('Merge Progress'), percent, message);
frm.page.set_indicator(__('In Progress'), 'orange');
});
frm.set_query("account", function(doc) {
if (!doc.company) frappe.throw(__('Please set Company'));
if (!doc.root_type) frappe.throw(__('Please set Root Type'));
return {
filters: {
root_type: doc.root_type,
company: doc.company
}
};
});
frm.set_query('account', 'merge_accounts', function(doc) {
if (!doc.company) frappe.throw(__('Please set Company'));
if (!doc.root_type) frappe.throw(__('Please set Root Type'));
if (!doc.account) frappe.throw(__('Please set Account'));
let acc = [doc.account];
frm.doc.merge_accounts.forEach((row) => {
acc.push(row.account);
});
return {
filters: {
is_group: doc.is_group,
root_type: doc.root_type,
name: ["not in", acc],
company: doc.company
}
};
});
},
refresh: function(frm) {
frm.page.hide_icon_group();
frm.trigger('set_merge_status');
frm.trigger('update_primary_action');
},
after_save: function(frm) {
setTimeout(() => {
frm.trigger('update_primary_action');
}, 500);
},
update_primary_action: function(frm) {
if (frm.is_dirty()) {
frm.enable_save();
return;
}
frm.disable_save();
if (frm.doc.status !== 'Success') {
if (!frm.is_new()) {
let label = frm.doc.status === 'Pending' ? __('Start Merge') : __('Retry');
frm.page.set_primary_action(label, () => frm.events.start_merge(frm));
} else {
frm.page.set_primary_action(__('Save'), () => frm.save());
}
}
},
start_merge: function(frm) {
frm.call({
method: 'form_start_merge',
args: { docname: frm.doc.name },
btn: frm.page.btn_primary
}).then(r => {
if (r.message === true) {
frm.disable_save();
}
});
},
set_merge_status: function(frm) {
if (frm.doc.status == "Pending") return;
let successful_records = 0;
frm.doc.merge_accounts.forEach((row) => {
if (row.merged) successful_records += 1;
});
let message_args = [successful_records, frm.doc.merge_accounts.length];
frm.dashboard.set_headline(__('Successfully merged {0} out of {1}.', message_args));
},
root_type: function(frm) {
frm.set_value('account', '');
frm.set_value('merge_accounts', []);
},
company: function(frm) {
frm.set_value('account', '');
frm.set_value('merge_accounts', []);
}
});
frappe.ui.form.on('Ledger Merge Accounts', {
merge_accounts_add: function(frm) {
frm.trigger('update_primary_action');
},
merge_accounts_remove: function(frm) {
frm.trigger('update_primary_action');
},
account: function(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
row.account_name = row.account;
frm.refresh_field('merge_accounts');
frm.trigger('update_primary_action');
}
});

View File

@ -0,0 +1,130 @@
{
"actions": [],
"autoname": "format:{account_name} merger on {creation}",
"creation": "2021-12-09 15:38:04.556584",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"section_break_1",
"root_type",
"account",
"account_name",
"column_break_3",
"company",
"status",
"is_group",
"section_break_5",
"merge_accounts"
],
"fields": [
{
"depends_on": "root_type",
"fieldname": "account",
"fieldtype": "Link",
"label": "Account",
"options": "Account",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "merge_accounts",
"fieldtype": "Table",
"label": "Accounts to Merge",
"options": "Ledger Merge Accounts",
"reqd": 1
},
{
"depends_on": "account",
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1,
"set_only_once": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Pending\nSuccess\nPartial Success\nError",
"read_only": 1
},
{
"fieldname": "root_type",
"fieldtype": "Select",
"label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
"reqd": 1,
"set_only_once": 1
},
{
"depends_on": "account",
"fetch_from": "account.account_name",
"fetch_if_empty": 1,
"fieldname": "account_name",
"fieldtype": "Data",
"label": "Account Name",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"depends_on": "account",
"fetch_from": "account.is_group",
"fieldname": "is_group",
"fieldtype": "Check",
"label": "Is Group",
"read_only": 1
}
],
"hide_toolbar": 1,
"links": [],
"modified": "2021-12-12 21:34:55.155146",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Ledger Merge",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,76 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.doctype.account.account import merge_account
class LedgerMerge(Document):
def start_merge(self):
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(
_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive")
)
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
enqueue(
start_merge,
queue="default",
timeout=6000,
event="ledger_merge",
job_name=self.name,
docname=self.name,
now=frappe.conf.developer_mode or frappe.flags.in_test,
)
return True
return False
@frappe.whitelist()
def form_start_merge(docname):
return frappe.get_doc("Ledger Merge", docname).start_merge()
def start_merge(docname):
ledger_merge = frappe.get_doc("Ledger Merge", docname)
successful_merges = 0
total = len(ledger_merge.merge_accounts)
for row in ledger_merge.merge_accounts:
if not row.merged:
try:
merge_account(
row.account,
ledger_merge.account,
ledger_merge.is_group,
ledger_merge.root_type,
ledger_merge.company
)
row.db_set('merged', 1)
frappe.db.commit()
successful_merges += 1
frappe.publish_realtime("ledger_merge_progress", {
"ledger_merge": ledger_merge.name,
"current": successful_merges,
"total": total
}
)
except Exception:
frappe.db.rollback()
frappe.log_error(title=ledger_merge.name)
finally:
if successful_merges == total:
ledger_merge.db_set('status', 'Success')
elif successful_merges > 0:
ledger_merge.db_set('status', 'Partial Success')
else:
ledger_merge.db_set('status', 'Error')
frappe.publish_realtime("ledger_merge_refresh", {"ledger_merge": ledger_merge.name})

View File

@ -0,0 +1,118 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from erpnext.accounts.doctype.ledger_merge.ledger_merge import start_merge
class TestLedgerMerge(unittest.TestCase):
def test_merge_success(self):
if not frappe.db.exists("Account", "Indirect Expenses - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Indirect Expenses"
acc.is_group = 1
acc.parent_account = "Expenses - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Indirect Test Expenses - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Indirect Test Expenses"
acc.is_group = 1
acc.parent_account = "Expenses - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Administrative Test Expenses - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Administrative Test Expenses"
acc.parent_account = "Indirect Test Expenses - _TC"
acc.company = "_Test Company"
acc.insert()
doc = frappe.get_doc({
"doctype": "Ledger Merge",
"company": "_Test Company",
"root_type": frappe.db.get_value("Account", "Indirect Test Expenses - _TC", "root_type"),
"account": "Indirect Expenses - _TC",
"merge_accounts": [
{
"account": "Indirect Test Expenses - _TC",
"account_name": "Indirect Expenses"
}
]
}).insert(ignore_permissions=True)
parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
self.assertEqual(parent, "Indirect Test Expenses - _TC")
start_merge(doc.name)
parent = frappe.db.get_value("Account", "Administrative Test Expenses - _TC", "parent_account")
self.assertEqual(parent, "Indirect Expenses - _TC")
self.assertFalse(frappe.db.exists("Account", "Indirect Test Expenses - _TC"))
def test_partial_merge_success(self):
if not frappe.db.exists("Account", "Indirect Income - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Indirect Income"
acc.is_group = 1
acc.parent_account = "Income - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Indirect Test Income - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Indirect Test Income"
acc.is_group = 1
acc.parent_account = "Income - _TC"
acc.company = "_Test Company"
acc.insert()
if not frappe.db.exists("Account", "Administrative Test Income - _TC"):
acc = frappe.new_doc("Account")
acc.account_name = "Administrative Test Income"
acc.parent_account = "Indirect Test Income - _TC"
acc.company = "_Test Company"
acc.insert()
doc = frappe.get_doc({
"doctype": "Ledger Merge",
"company": "_Test Company",
"root_type": frappe.db.get_value("Account", "Indirect Income - _TC", "root_type"),
"account": "Indirect Income - _TC",
"merge_accounts": [
{
"account": "Indirect Test Income - _TC",
"account_name": "Indirect Test Income"
},
{
"account": "Administrative Test Income - _TC",
"account_name": "Administrative Test Income"
}
]
}).insert(ignore_permissions=True)
parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
self.assertEqual(parent, "Indirect Test Income - _TC")
start_merge(doc.name)
parent = frappe.db.get_value("Account", "Administrative Test Income - _TC", "parent_account")
self.assertEqual(parent, "Indirect Income - _TC")
self.assertFalse(frappe.db.exists("Account", "Indirect Test Income - _TC"))
self.assertTrue(frappe.db.exists("Account", "Administrative Test Income - _TC"))
def tearDown(self):
for entry in frappe.db.get_all("Ledger Merge"):
frappe.delete_doc("Ledger Merge", entry.name)
test_accounts = [
"Indirect Test Expenses - _TC",
"Administrative Test Expenses - _TC",
"Indirect Test Income - _TC",
"Administrative Test Income - _TC"
]
for account in test_accounts:
frappe.delete_doc_if_exists("Account", account)

View File

@ -0,0 +1,52 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-12-09 15:44:58.033398",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"account_name",
"merged"
],
"fields": [
{
"columns": 4,
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account",
"reqd": 1
},
{
"columns": 2,
"default": "0",
"fieldname": "merged",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Merged",
"read_only": 1
},
{
"columns": 4,
"fieldname": "account_name",
"fieldtype": "Data",
"label": "Account Name",
"read_only": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-12-10 15:27:24.477139",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Ledger Merge Accounts",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Wahni Green Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class LedgerMergeAccounts(Document):
pass

View File

@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
"modified": "2019-07-25 14:57:33.187689",
"modified": "2022-01-04 15:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",

View File

@ -135,7 +135,7 @@ class OpeningInvoiceCreationTool(Document):
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty)
return frappe._dict({
item_dict = frappe._dict({
"uom": default_uom,
"rate": rate or 0.0,
"qty": row.qty,
@ -146,6 +146,13 @@ class OpeningInvoiceCreationTool(Document):
"cost_center": cost_center
})
for dimension in get_accounting_dimensions():
item_dict.update({
dimension: row.get(dimension)
})
return item_dict
item = get_item_dict()
invoice = frappe._dict({
@ -166,7 +173,7 @@ class OpeningInvoiceCreationTool(Document):
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
invoice.update({
dimension: item.get(dimension)
dimension: self.get(dimension) or item.get(dimension)
})
return invoice

View File

@ -7,21 +7,26 @@ import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
test_dependencies = ["Customer", "Supplier"]
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
doc.update(args)
return doc.make_invoices()
@ -106,6 +111,19 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
def test_opening_invoice_with_accounting_dimension(self):
invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"],
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
}
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
def tearDown(self):
disable_dimension()
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")

View File

@ -2,7 +2,7 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe import _, bold
from frappe.model.document import Document
@ -12,6 +12,17 @@ class PartyLink(Document):
frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."),
title=_("Invalid Primary Role"))
existing_party_link = frappe.get_all('Party Link', {
'primary_party': self.primary_party,
'secondary_party': self.secondary_party
}, pluck="primary_role")
if existing_party_link:
frappe.throw(_('{} {} is already linked with {} {}')
.format(
self.primary_role, bold(self.primary_party),
self.secondary_role, bold(self.secondary_party)
))
existing_party_link = frappe.get_all('Party Link', {
'primary_party': self.secondary_party
}, pluck="primary_role")

View File

@ -3,6 +3,7 @@
import json
from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
@ -1523,6 +1524,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
pe.project = (doc.get('project') or
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
@ -1708,7 +1713,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':

View File

@ -15,6 +15,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@ -123,9 +124,26 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
bold_invalid_batch_no = frappe.bold(item.batch_no)
if (available_batch_qty - reserved_batch_qty) == 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
@ -139,6 +157,20 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_invalid_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
error_msg = []
invalid_serials, msg = "", ""
for serial_no in serial_nos:
if not frappe.db.exists('Serial No', serial_no):
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
msg = (_("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
if invalid_serials:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self):
if self.is_return or self.docstatus != 1:
return
@ -150,6 +182,9 @@ class POSInvoice(SalesInvoice):
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
self.validate_invalid_serial_nos(d)
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else:
if allow_negative_stock:
return
@ -326,7 +361,6 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer:
self.customer = profile.customer
self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse

View File

@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase):
pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit)
def test_invalid_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(company='_Test Company',
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
serial_nos = se.get("items")[0].serial_no + 'wrong'
pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1)
pos.get('items')[0].has_serial_no = 1
pos.get('items')[0].serial_no = serial_nos
pos.insert()
self.assertRaises(frappe.ValidationError, pos.submit)
def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,
@ -521,6 +539,72 @@ class TestPOSInvoice(unittest.TestCase):
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
def test_pos_batch_item_qty_validation(self):
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_batch_item_with_batch,
)
create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01')
item = frappe.get_doc('Item', '_BATCH ITEM')
batch = frappe.get_doc('Batch', 'TestBatch 01')
batch.submit()
item.batch_no = 'TestBatch 01'
item.save()
se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01')
pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1)
pos_inv1.items[0].batch_no = 'TestBatch 01'
pos_inv1.save()
pos_inv1.submit()
pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1)
pos_inv2.items[0].batch_no = 'TestBatch 01'
pos_inv2.save()
self.assertRaises(frappe.ValidationError, pos_inv2.submit)
#teardown
pos_inv1.reload()
pos_inv1.cancel()
pos_inv1.delete()
pos_inv2.reload()
pos_inv2.delete()
se.cancel()
batch.reload()
batch.cancel()
batch.delete()
def test_ignore_pricing_rule(self):
from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule
item_price = frappe.get_doc({
'doctype': 'Item Price',
'item_code': '_Test Item',
'price_list': '_Test Price List',
'price_list_rate': '450',
})
item_price.insert()
pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10)
pr.save()
pos_inv = create_pos_invoice(qty=1, do_not_submit=1)
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.items[0].discount_percentage, 10)
# rate shouldn't change
self.assertEquals(pos_inv.items[0].rate, 405)
pos_inv.ignore_pricing_rule = 1
pos_inv.items[0].rate = 300
pos_inv.save()
self.assertEquals(pos_inv.ignore_pricing_rule, 1)
# rate should change since pricing rules are ignored
self.assertEquals(pos_inv.items[0].rate, 300)
item_price.delete()
pos_inv.delete()
pr.delete()
def create_pos_invoice(**args):
args = frappe._dict(args)
pos_profile = None
@ -557,7 +641,8 @@ def create_pos_invoice(**args):
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no
"serial_no": args.serial_no,
"batch_no": args.batch_no
})
if not args.do_not_save:
@ -570,3 +655,8 @@ def create_pos_invoice(**args):
pos_inv.payment_schedule = []
return pos_inv
def make_batch_item(item_name):
from erpnext.stock.doctype.item.test_item import make_item
if not frappe.db.exists(item_name):
return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1))

View File

@ -166,7 +166,7 @@ class TestPricingRule(unittest.TestCase):
"item_group": "Products",
},
{
"item_group": "Seed",
"item_group": "_Test Item Group",
},
],
"selling": 1,
@ -628,6 +628,26 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]:
doc.delete()
def test_multiple_pricing_rules_with_min_qty(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
item = si.items[0]
item.stock_qty = 1
si.save()
self.assertFalse(item.discount_percentage)
item.qty = 5
item.stock_qty = 5
si.save()
self.assertEqual(item.discount_percentage, 30)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
test_dependencies = ["Campaign"]
def make_pricing_rule(**args):
@ -650,7 +670,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
@ -676,6 +696,8 @@ def make_pricing_rule(**args):
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
return doc
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({

View File

@ -73,7 +73,7 @@ def sorted_by_priority(pricing_rules, args, doc=None):
for key in sorted(pricing_rule_dict):
pricing_rules_list.extend(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules
return pricing_rules_list
def filter_pricing_rule_based_on_condition(pricing_rules, doc=None):
filtered_pricing_rules = []

View File

@ -505,11 +505,11 @@ class PurchaseInvoice(BuyingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Did not use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
@ -517,8 +517,8 @@ class PurchaseInvoice(BuyingController):
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,

View File

@ -1236,7 +1236,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(fiscal_year='2021')
fiscal_year = get_fiscal_year(date=nowdate())
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),

View File

@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", {
setup: function(frm) {

View File

@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
};
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
const generate_irns = () => {
const docnames = list_view.get_checked_items(true);
if (docnames && docnames.length) {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
args: { docnames },
freeze: true,
freeze_message: __('Generating E-Invoices...')
});
} else {
frappe.msgprint({
message: __('Please select at least one sales invoice to generate IRN'),
title: __('No Invoice Selected'),
indicator: 'red'
});
}
};
const cancel_irns = () => {
const docnames = list_view.get_checked_items(true);
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
args: {
doctype: list_view.doctype,
docnames,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
freeze_message: __('Cancelling E-Invoices...'),
});
d.hide();
},
primary_action_label: __('Submit')
});
d.show();
};
let einvoicing_enabled = false;
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
einvoicing_enabled = enabled;
});
list_view.$result.on("change", "input[type=checkbox]", () => {
if (einvoicing_enabled) {
const docnames = list_view.get_checked_items(true);
// show/hide e-invoicing actions when no sales invoices are checked
if (docnames && docnames.length) {
// prevent adding actions twice if e-invoicing action group already exists
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
}
} else {
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
}
}
});
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices generated successfully', [invoices.length]),
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to generate IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
});
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to cancel IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
});
};

View File

@ -469,7 +469,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
});
frm.trigger("calculate_timesheet_totals");
this.frm.trigger("calculate_timesheet_totals");
}
}
};

View File

@ -651,7 +651,7 @@
"hide_seconds": 1,
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
"permlevel": 0,
"print_hide": 1
},
{
@ -2038,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-10-21 20:19:38.667508",
"modified": "2021-12-23 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -43,6 +43,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@ -293,6 +294,8 @@ class SalesInvoice(SellingController):
def before_cancel(self):
self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None)
def on_cancel(self):
@ -728,8 +731,11 @@ class SalesInvoice(SellingController):
def update_packing_list(self):
if cint(self.update_stock) == 1:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
if cint(self.is_return) and self.return_against:
calculate_mapped_packed_items_return(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
else:
self.set('packed_items', [])
@ -862,11 +868,11 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.debit_to,
@ -874,8 +880,8 @@ class SalesInvoice(SellingController):
"party": self.customer,
"due_date": self.due_date,
"against": self.against_income_account,
"debit": grand_total_in_company_currency,
"debit_in_account_currency": grand_total_in_company_currency \
"debit": base_grand_total,
"debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,

View File

@ -20,6 +20,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.regional.india.utils import get_ewb_data
@ -1780,47 +1781,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
frappe.set_user("Administrator")
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
self.assertRaises(frappe.ValidationError, pda1.submit)
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
@ -2140,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self):
# init
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
si.irn = 'test_irn'
si.submit()
# reset
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
self.assertTrue(einvoice['EwbDtls'])
validate_totals(einvoice)
si.apply_discount_on = 'Net Total'
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
[d.set('included_in_print_rate', 1) for d in si.taxes]
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
def test_item_tax_net_range(self):
item = create_item("T Shirt")
@ -2232,9 +2240,9 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.48],
["2021-09-30", 5041.1, 26352.58]
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
["2021-09-30", 5041.1, 26407.22]
]
for i, schedule in enumerate(asset.schedules):
@ -2282,12 +2290,12 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48, True],
["2021-06-30", 20000.0, 21311.48, True],
["2022-06-30", 20000.0, 41311.48, False],
["2023-06-30", 20000.0, 61311.48, False],
["2024-06-30", 20000.0, 81311.48, False],
["2025-06-06", 18688.52, 100000.0, False]
["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61366.12, False],
["2024-06-30", 20000.0, 81366.12, False],
["2025-06-06", 18633.88, 100000.0, False]
]
for i, schedule in enumerate(asset.schedules):
@ -2385,6 +2393,41 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_update_invoice_status(self):
today = nowdate()
# Sales Invoice without Payment Schedule
si = create_sales_invoice(posting_date=add_days(today, -5))
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend("payment_schedule", [
{
"due_date": add_days(today, -5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
},
{
"due_date": add_days(today, 5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
}
])
si_with_payment_schedule.submit()
for invoice in (si, si_with_payment_schedule):
invoice.db_set("status", "Unpaid")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue")
invoice.db_set("status", "Unpaid and Discounted")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue and Discounted")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
item = copy.deepcopy(si.get('items')[0])
@ -2446,6 +2489,74 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
si.set_posting_time = 1
si.posting_date = '2019-01-01'
si.debit_to = '_Test Receivable USD - _TC'
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-01"
si.items[0].service_end_date = "2019-03-30"
si.items[0].deferred_expense_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
pda1.submit()
expected_gle = [
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
[deferred_account, 2089.89, 0.0, "2019-01-28"],
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
[deferred_account, 1887.64, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
[deferred_account, 2022.47, 0.0, "2019-03-15"]
]
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.credit)
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@ -71,7 +71,8 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
if shipping_amount:
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"):

View File

@ -28,14 +28,14 @@
{
"columns": 2,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Single Transaction Threshold"
},
{
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
},
@ -59,7 +59,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-31 11:42:12.213977",
"modified": "2022-01-13 12:04:42.904263",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
@ -68,5 +68,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -2,15 +2,17 @@
"creation": "2021-08-24 12:28:18.044902",
"docstatus": 0,
"doctype": "Form Tour",
"first_document": 0,
"idx": 0,
"include_name_field": 0,
"is_standard": 1,
"modified": "2021-08-24 12:28:18.044902",
"modified": "2022-01-18 18:32:17.102330",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
"owner": "Administrator",
"reference_doctype": "Sales Taxes and Charges Template",
"save_on_complete": 0,
"save_on_complete": 1,
"steps": [
{
"description": "A name by which you will identify this template. You can change this later.",

View File

@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt
import copy
import frappe
from frappe import _
from frappe.model.meta import get_field_precision
@ -51,49 +53,57 @@ def validate_accounting_period(gl_map):
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True, precision=None):
if not gl_map:
return []
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
if merge_entries:
gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0:
entry.credit_in_account_currency = \
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = \
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
update_net_values(entry)
gl_map = toggle_debit_credit_if_negative(gl_map)
return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = entry.debit_in_account_currency \
- entry.credit_in_account_currency
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = entry.credit_in_account_currency \
- entry.debit_in_account_currency
def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
if not cost_center_allocation:
return gl_map
entry.debit = 0
entry.debit_in_account_currency = 0
new_gl_map = []
for d in gl_map:
cost_center = d.get("cost_center")
if cost_center and cost_center_allocation.get(cost_center):
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
new_gl_map.append(gle)
else:
new_gl_map.append(d)
return new_gl_map
def get_cost_center_allocation_data(company, posting_date):
par = frappe.qb.DocType("Cost Center Allocation")
child = frappe.qb.DocType("Cost Center Allocation Percentage")
records = (
frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
.select(par.main_cost_center, child.cost_center, child.percentage)
.where(par.docstatus == 1)
.where(par.company == company)
.where(par.valid_from <= posting_date)
.orderby(par.valid_from, order=frappe.qb.desc)
).run(as_dict=True)
cc_allocation = frappe._dict()
for d in records:
cc_allocation.setdefault(d.main_cost_center, frappe._dict())\
.setdefault(d.cost_center, d.percentage)
return cc_allocation
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
@ -145,6 +155,49 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head:
return e
def toggle_debit_credit_if_negative(gl_map):
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0:
entry.credit_in_account_currency = \
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = \
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
update_net_values(entry)
return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = entry.debit_in_account_currency \
- entry.credit_in_account_currency
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = entry.credit_in_account_currency \
- entry.debit_in_account_currency
entry.debit = 0
entry.debit_in_account_currency = 0
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost:
validate_cwip_accounts(gl_map)

View File

@ -13,15 +13,12 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
"idx": 0,
"is_complete": 0,
"modified": "2021-08-13 11:59:35.690443",
"modified": "2022-01-18 18:35:52.326688",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts",
"owner": "Administrator",
"steps": [
{
"step": "Company"
},
{
"step": "Chart of Accounts"
},

View File

@ -1,22 +0,0 @@
{
"action": "Go to Page",
"action_label": "Let's Review your Company",
"creation": "2021-06-29 14:47:42.497318",
"description": "# Company\n\nIn ERPNext, you can also create multiple companies, and establish relationships (group/subsidiary) among them.\n\nWithin the company master, you can capture various default accounts for that Company and set crucial settings related to the accounting methodology followed for a company. \n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:43:35.767341",
"modified_by": "Administrator",
"name": "Company",
"owner": "Administrator",
"path": "app/company",
"reference_document": "Company",
"show_form_tour": 0,
"show_full_form": 0,
"title": "Review Company",
"validate_action": 1
}

View File

@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
set_contact_details(party_details, party, party_type)

View File

@ -0,0 +1,173 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
<div class="print-heading">
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
</div>
</div>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipped From</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
{%- if einvoice.DispDtls -%}
{%- set dispatch = einvoice.DispDtls -%}
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
<p>{{ dispatch.LglNm }}</p>
<p>{{ dispatch.Addr1 }}</p>
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
<p>{{ dispatch.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
{% endif %}
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,24 @@
{
"align_labels_right": 1,
"creation": "2020-10-10 18:01:21.032914",
"custom_format": 0,
"default_print_language": "en-US",
"disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "",
"idx": 0,
"line_breaks": 1,
"modified": "2020-10-23 19:54:40.634936",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST E-Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 1,
"standard": "Yes"
}

View File

@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = {
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname":"show_gl_balance",
"label": __("Show GL Balance"),
"fieldtype": "Check",
},
],
onload: function(report) {

View File

@ -4,7 +4,7 @@
import frappe
from frappe import _, scrub
from frappe.utils import cint
from frappe.utils import cint, flt
from erpnext.accounts.party import get_partywise_advanced_payment_amount
from erpnext.accounts.report.accounts_receivable.accounts_receivable import ReceivablePayableReport
@ -36,6 +36,9 @@ class AccountsReceivableSummary(ReceivablePayableReport):
party_advance_amount = get_partywise_advanced_payment_amount(self.party_type,
self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {}
if self.filters.show_gl_balance:
gl_balance_map = get_gl_balance(self.filters.report_date)
for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
continue
@ -55,6 +58,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# but in summary report advance shown in separate column
row.paid -= row.advance
if self.filters.show_gl_balance:
row.gl_balance = gl_balance_map.get(party)
row.diff = flt(row.outstanding) - flt(row.gl_balance)
self.data.append(row)
def get_party_total(self, args):
@ -114,6 +121,10 @@ class AccountsReceivableSummary(ReceivablePayableReport):
self.add_column(_(credit_debit_label), fieldname='credit_note')
self.add_column(_('Outstanding Amount'), fieldname='outstanding')
if self.filters.show_gl_balance:
self.add_column(_('GL Balance'), fieldname='gl_balance')
self.add_column(_('Difference'), fieldname='diff')
self.setup_ageing_columns()
if self.party_type == "Customer":
@ -140,3 +151,7 @@ class AccountsReceivableSummary(ReceivablePayableReport):
# Add column for total due amount
self.add_column(label="Total Amount Due", fieldname='total_due')
def get_gl_balance(report_date):
return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'],
filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1))

View File

@ -29,18 +29,6 @@ def execute(filters=None):
dimension_items = cam_map.get(dimension)
if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
else:
DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(dimension)s
AND parent NOT IN %(dimension)s
GROUP BY parent''',{'dimension':[dimension]})
if DCC_allocation:
filters['budget_against_filter'] = [DCC_allocation[0][0]]
ddc_cam_map = get_dimension_account_month_map(filters)
dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
chart = get_chart_data(filters, columns, data)

View File

@ -121,20 +121,21 @@ class Deferred_Item(object):
"""
simulate future posting by creating dummy gl entries. starts from the last posting date.
"""
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.period_list[-1].to_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
if self.service_start_date != self.service_end_date:
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
self.estimate_for_period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
add_days(self.last_entry_date, 1),
self.period_list[-1].to_date,
"Date Range",
"Monthly",
company=self.filters.company,
)
for period in self.estimate_for_period_list:
amount = self.calculate_amount(period.from_date, period.to_date)
gle = self.make_dummy_gle(period.key, period.to_date, amount)
self.gle_entries.append(gle)
def calculate_item_revenue_expense_for_period(self):
"""

View File

@ -17,10 +17,42 @@ from erpnext.stock.doctype.item.test_item import create_item
class TestDeferredRevenueAndExpense(unittest.TestCase):
@classmethod
def setUpClass(self):
clear_old_entries()
clear_accounts_and_items()
create_company()
self.maxDiff = None
def clear_old_entries(self):
sinv = qb.DocType("Sales Invoice")
sinv_item = qb.DocType("Sales Invoice Item")
pinv = qb.DocType("Purchase Invoice")
pinv_item = qb.DocType("Purchase Invoice Item")
# delete existing invoices with deferred items
deferred_invoices = (
qb.from_(sinv)
.join(sinv_item)
.on(sinv.name == sinv_item.parent)
.select(sinv.name)
.where(sinv_item.enable_deferred_revenue == 1)
.run()
)
if deferred_invoices:
qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
deferred_invoices = (
qb.from_(pinv)
.join(pinv_item)
.on(pinv.name == pinv_item.parent)
.select(pinv.name)
.where(pinv_item.enable_deferred_expense == 1)
.run()
)
if deferred_invoices:
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
def test_deferred_revenue(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
@ -108,6 +140,8 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
self.assertEqual(report.period_total, expected)
def test_deferred_expense(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_expense_account = create_account(
account_name="Deferred Expense",
@ -198,6 +232,91 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
]
self.assertEqual(report.period_total, expected)
def test_zero_months(self):
self.clear_old_entries()
# created deferred expense accounts, if not found
deferred_revenue_account = create_account(
account_name="Deferred Revenue",
parent_account="Current Liabilities - _CD",
company="_Test Company DR",
)
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
acc_settings.book_deferred_entries_based_on = "Months"
acc_settings.save()
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test Customer DR"
customer.type = "Individual"
customer.insert()
item = create_item(
"_Test Internet Subscription",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_revenue_account
item.no_of_months = 0
item.save()
si = create_sales_invoice(
item=item.name,
company="_Test Company DR",
customer="_Test Customer DR",
debit_to="Debtors - _CD",
posting_date="2021-05-01",
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_submit=True,
rate=300,
price_list_rate=300,
)
si.items[0].enable_deferred_revenue = 1
si.items[0].deferred_revenue_account = deferred_revenue_account
si.items[0].income_account = "Sales - _CD"
si.save()
si.submit()
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date=nowdate(),
start_date="2021-05-01",
end_date="2021-08-01",
type="Income",
company="_Test Company DR",
)
)
pda.insert()
pda.submit()
# execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
self.filters = frappe._dict(
{
"company": frappe.defaults.get_user_default("Company"),
"filter_based_on": "Date Range",
"period_start_date": "2021-05-01",
"period_end_date": "2021-08-01",
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Revenue",
"with_upcoming_postings": False,
}
)
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
report.run()
expected = [
{"key": "may_2021", "total": 300.0, "actual": 300.0},
{"key": "jun_2021", "total": 0, "actual": 0},
{"key": "jul_2021", "total": 0, "actual": 0},
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
def create_company():
company = frappe.db.exists("Company", "_Test Company DR")
@ -209,15 +328,11 @@ def create_company():
company.insert()
def clear_old_entries():
def clear_accounts_and_items():
item = qb.DocType("Item")
account = qb.DocType("Account")
customer = qb.DocType("Customer")
supplier = qb.DocType("Supplier")
sinv = qb.DocType("Sales Invoice")
sinv_item = qb.DocType("Sales Invoice Item")
pinv = qb.DocType("Purchase Invoice")
pinv_item = qb.DocType("Purchase Invoice Item")
qb.from_(account).delete().where(
(account.account_name == "Deferred Revenue")
@ -228,26 +343,3 @@ def clear_old_entries():
).run()
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
# delete existing invoices with deferred items
deferred_invoices = (
qb.from_(sinv)
.join(sinv_item)
.on(sinv.name == sinv_item.parent)
.select(sinv.name)
.where(sinv_item.enable_deferred_revenue == 1)
.run()
)
if deferred_invoices:
qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
deferred_invoices = (
qb.from_(pinv)
.join(pinv_item)
.on(pinv.name == pinv_item.parent)
.select(pinv.name)
.where(pinv_item.enable_deferred_expense == 1)
.run()
)
if deferred_invoices:
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()

View File

@ -387,42 +387,15 @@ def set_gl_entries_by_account(
key: value
})
distributed_cost_center_query = ""
if filters and filters.get('cost_center'):
distributed_cost_center_query = """
UNION ALL
SELECT posting_date,
account,
debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
is_opening,
fiscal_year,
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency,
account_currency
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{additional_conditions}
AND posting_date <= %(to_date)s
AND is_cancelled = 0
AND cost_center = DCC_allocation.parent
""".format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
gl_entries = frappe.db.sql("""
select posting_date, account, debit, credit, is_opening, fiscal_year,
debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
where company=%(company)s
{additional_conditions}
and posting_date <= %(to_date)s
and is_cancelled = 0
{distributed_cost_center_query}""".format(
additional_conditions=additional_conditions,
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
and is_cancelled = 0""".format(
additional_conditions=additional_conditions), gl_filters, as_dict=True
)
if filters and filters.get('presentation_currency'):
convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))

View File

@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "include_dimensions",
"label": __("Consider Accounting Dimensions"),
"fieldtype": "Check",
"default": 0
"default": 1
},
{
"fieldname": "show_opening_entries",

View File

@ -176,44 +176,7 @@ def get_gl_entries(filters, accounting_dimensions):
if accounting_dimensions:
dimension_fields = ', '.join(accounting_dimensions) + ','
distributed_cost_center_query = ""
if filters and filters.get('cost_center'):
select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
UNION ALL
SELECT name as gl_entry,
posting_date,
account,
party_type,
party,
voucher_type,
voucher_no, {dimension_fields}
cost_center, project,
against_voucher_type,
against_voucher,
account_currency,
remarks, against,
is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{conditions}
AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent
""".format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql(
"""
gl_entries = frappe.db.sql("""
select
name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, {dimension_fields}
@ -222,13 +185,11 @@ def get_gl_entries(filters, accounting_dimensions):
remarks, against, is_opening, creation {select_fields}
from `tabGL Entry`
where company=%(company)s {conditions}
{distributed_cost_center_query}
{order_by_statement}
""".format(
dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query,
order_by_statement=order_by_statement
),
filters, as_dict=1)
""".format(
dimension_fields=dimension_fields, select_fields=select_fields,
conditions=get_conditions(filters), order_by_statement=order_by_statement
), filters, as_dict=1)
if filters.get('presentation_currency'):
return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))
@ -448,9 +409,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist)
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
@ -547,10 +510,7 @@ def get_columns(filters):
"fieldname": "balance",
"fieldtype": "Float",
"width": 130
}
]
columns.extend([
},
{
"label": _("Voucher Type"),
"fieldname": "voucher_type",
@ -584,7 +544,7 @@ def get_columns(filters):
"fieldname": "project",
"width": 100
}
])
]
if filters.get("include_dimensions"):
for dim in get_accounting_dimensions(as_list = False):
@ -594,14 +554,14 @@ def get_columns(filters):
"fieldname": dim.fieldname,
"width": 100
})
columns.extend([
{
columns.append({
"label": _("Cost Center"),
"options": "Cost Center",
"fieldname": "cost_center",
"width": 100
},
})
columns.extend([
{
"label": _("Against Voucher Type"),
"fieldname": "against_voucher_type",

View File

@ -109,7 +109,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
data = []
new_accounts = accounts
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
for d in accounts:
@ -123,19 +122,6 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
"currency": company_currency,
"based_on": based_on
}
if based_on == 'cost_center':
cost_center_doc = frappe.get_doc("Cost Center",d.name)
if not cost_center_doc.enable_distributed_cost_center:
DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent""",{'cost_center': [d.name]})
if DCC_allocation:
for account in new_accounts:
if account['name'] == DCC_allocation[0][0]:
for value in value_fields:
d[value] += account[value]*(DCC_allocation[0][1]/100)
for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3)

View File

@ -0,0 +1,48 @@
import unittest
from typing import List, Tuple
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
"period_start_date": "2010-01-01",
"period_end_date": "2030-01-01"
}
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("General Ledger", {"group_by": "Group by Voucher (Consolidated)"} ),
("General Ledger", {"group_by": "Group by Voucher (Consolidated)", "include_dimensions": 1} ),
("Accounts Payable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
("Accounts Receivable", {"range1": 30, "range2": 60, "range3": 90, "range4": 120}),
("Consolidated Financial Statement", {"report": "Balance Sheet"} ),
("Consolidated Financial Statement", {"report": "Profit and Loss Statement"} ),
("Consolidated Financial Statement", {"report": "Cash Flow"} ),
("Gross Profit", {"group_by": "Invoice"}),
("Gross Profit", {"group_by": "Item Code"}),
("Gross Profit", {"group_by": "Item Group"}),
("Gross Profit", {"group_by": "Customer"}),
("Gross Profit", {"group_by": "Customer Group"}),
("Item-wise Sales Register", {}),
("Item-wise Purchase Register", {}),
("Sales Register", {}),
("Purchase Register", {}),
("Tax Detail", {"mode": "run", "report_name": "Tax Detail"},),
]
OPTIONAL_FILTERS = {}
class TestReports(unittest.TestCase):
def test_execute_all_accounts_reports(self):
"""Test that all script report in stock modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report(
report_name=report,
module="Accounts",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@ -5,7 +5,7 @@
"label": "Profit and Loss"
}
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Accounts\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Profit and Loss\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chart of Accounts\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Journal Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payment Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Trial Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounting Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Payable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Financial Statements\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Multi Currency\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bank Statement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Subscription Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goods and Services Tax (GST India)\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Share Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Cost Center and Budgeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening and Closing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Profitability\", \"col\": 4}}]",
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Goods and Services Tax (GST India)\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]",
"creation": "2020-03-02 15:41:59.515192",
"docstatus": 0,
"doctype": "Workspace",
@ -230,6 +230,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Payment Reconciliation",
"link_count": 0,
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"onboard": 0,
@ -346,6 +347,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Payment Reconciliation",
"link_count": 0,
"link_to": "Payment Reconciliation",
"link_type": "DocType",
"onboard": 0,
@ -527,16 +529,17 @@
"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"
},
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "KSA VAT Report",
"link_count": 0,
"link_to": "KSA VAT",
"link_type": "Report",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1020,6 +1023,17 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center Allocation",
"link_count": 0,
"link_to": "Cost Center Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Cost Center",
"hidden": 0,
@ -1158,15 +1172,16 @@
"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,
"is_query_report": 0,
"label": "KSA VAT Setting",
"link_count": 0,
"link_to": "KSA VAT Setting",
"link_type": "DocType",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1220,7 +1235,7 @@
"type": "Link"
}
],
"modified": "2021-08-27 12:15:52.872471",
"modified": "2022-01-13 17:25:09.835345",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@ -1229,7 +1244,7 @@
"public": 1,
"restrict_to_domain": "",
"roles": [],
"sequence_id": 2,
"sequence_id": 2.0,
"shortcuts": [
{
"label": "Chart of Accounts",
@ -1278,4 +1293,4 @@
}
],
"title": "Accounting"
}
}

View File

@ -1,8 +0,0 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Agriculture Analysis Criteria', {
refresh: function(frm) {
}
});

View File

@ -1,182 +0,0 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:title",
"beta": 0,
"creation": "2017-12-05 16:37:46.599982",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "title",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Title",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "standard",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Standard",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "linked_doctype",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Linked Doctype",
"length": 0,
"no_copy": 0,
"options": "\nWater Analysis\nSoil Analysis\nPlant Analysis\nFertilizer\nSoil Texture\nWeather",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-11-04 03:27:36.678832",
"modified_by": "Administrator",
"module": "Agriculture",
"name": "Agriculture Analysis Criteria",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Agriculture Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Agriculture User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Agriculture",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class AgricultureAnalysisCriteria(Document):
pass

View File

@ -1,8 +0,0 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestAgricultureAnalysisCriteria(unittest.TestCase):
pass

View File

@ -1,8 +0,0 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Agriculture Task', {
refresh: function(frm) {
}
});

View File

@ -1,212 +0,0 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "AG-TASK-.#####",
"beta": 0,
"creation": "2017-10-26 15:51:19.602452",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "task_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Task Name",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "start_day",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Start Day",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "end_day",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "End Day",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Ignore holidays",
"fieldname": "holiday_management",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Holiday Management",
"length": 0,
"no_copy": 0,
"options": "Ignore holidays\nPrevious Business Day\nNext Business Day",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Low",
"fieldname": "priority",
"fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Priority",
"length": 0,
"no_copy": 0,
"options": "Low\nMedium\nHigh\nUrgent",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-11-04 03:28:08.679157",
"modified_by": "Administrator",
"module": "Agriculture",
"name": "Agriculture Task",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Agriculture",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class AgricultureTask(Document):
pass

View File

@ -1,8 +0,0 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestAgricultureTask(unittest.TestCase):
pass

View File

@ -1,55 +0,0 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.provide("erpnext.crop");
frappe.ui.form.on('Crop', {
refresh: (frm) => {
frm.fields_dict.materials_required.grid.set_column_disp('bom_no', false);
}
});
frappe.ui.form.on("BOM Item", {
item_code: (frm, cdt, cdn) => {
erpnext.crop.update_item_rate_uom(frm, cdt, cdn);
},
qty: (frm, cdt, cdn) => {
erpnext.crop.update_item_qty_amount(frm, cdt, cdn);
},
rate: (frm, cdt, cdn) => {
erpnext.crop.update_item_qty_amount(frm, cdt, cdn);
}
});
erpnext.crop.update_item_rate_uom = function(frm, cdt, cdn) {
let material_list = ['materials_required', 'produce', 'byproducts'];
material_list.forEach((material) => {
frm.doc[material].forEach((item, index) => {
if (item.name == cdn && item.item_code){
frappe.call({
method:'erpnext.agriculture.doctype.crop.crop.get_item_details',
args: {
item_code: item.item_code
},
callback: (r) => {
frappe.model.set_value('BOM Item', item.name, 'uom', r.message.uom);
frappe.model.set_value('BOM Item', item.name, 'rate', r.message.rate);
}
});
}
});
});
};
erpnext.crop.update_item_qty_amount = function(frm, cdt, cdn) {
let material_list = ['materials_required', 'produce', 'byproducts'];
material_list.forEach((material) => {
frm.doc[material].forEach((item, index) => {
if (item.name == cdn){
if (!frappe.model.get_value('BOM Item', item.name, 'qty'))
frappe.model.set_value('BOM Item', item.name, 'qty', 1);
frappe.model.set_value('BOM Item', item.name, 'amount', item.qty * item.rate);
}
});
});
};

Some files were not shown because too many files have changed in this diff Show More