Merge branch 'develop' of https://github.com/frappe/erpnext into term_loan_enhancement

This commit is contained in:
Deepesh Garg 2021-12-28 17:14:24 +05:30
commit be5e6f02ae
509 changed files with 10784 additions and 11934 deletions

View File

@ -1,47 +0,0 @@
---
name: Bug report
about: Report a bug encountered while using ERPNext
labels: bug
---
<!--
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->
## Description of the issue
## Context information (for bug reports)
**Output of `bench version`**
```
(paste here)
```
## Steps to reproduce the issue
1.
2.
3.
### Observed result
### Expected result
### Stacktrace / full error message
```
(paste here)
```
## Additional information
OS version / distribution, `ERPNext` install method, etc.

87
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,87 @@
name: Bug Report
description: Report a bug encountered while using ERPNext
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
- type: textarea
id: bug-info
attributes:
label: Information about bug
description: Also tell us, what did you expect to happen?
placeholder: Please provide as much information as possible.
validations:
required: true
- type: dropdown
id: module
attributes:
label: Module
description: Select affected module of ERPNext.
multiple: true
options:
- accounts
- stock
- buying
- selling
- ecommerce
- manufacturing
- HR
- projects
- support
- assets
- integrations
- quality
- regional
- portal
- agriculture
- education
- non-profit
validations:
required: true
- type: textarea
id: exact-version
attributes:
label: Version
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext Verion -
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- docker
- easy-install
- manual install
- FrappeCloud
validations:
required: false
- type: textarea
id: logs
attributes:
label: Relevant log output / Stack trace / Full Error Message.
description: Please copy and paste any relevant log output. This will be automatically formatted.
render: shell
- type: markdown
attributes:
value: |
By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)

View File

@ -1,7 +1,10 @@
---
name: Feature request
about: Suggest an idea to improve ERPNext
title: ''
labels: feature-request
assignees: ''
---
<!--

View File

@ -1,17 +0,0 @@
---
name: Question about using ERPNext
about: This is not the appropriate channel
labels: invalid
---
Please post on our forums:
for questions about using `ERPNext`: https://discuss.erpnext.com
for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**

56
.github/stale.yml vendored
View File

@ -1,34 +1,36 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 3
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- hotfix
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking as stale
staleLabel: inactive
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed within a week if no further activity occurs, but it
only takes a comment to keep a contribution alive :) Also, even if it is closed,
you can always reopen the PR when you're ready. Thank you for contributing.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
limitPerRun: 10
# Limit to only `issues` or `pulls`
only: pulls
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
pulls:
daysUntilStale: 15
daysUntilClose: 3
exemptLabels:
- hotfix
markComment: >
This pull request has been automatically marked as inactive because it has
not had recent activity. It will be closed within 3 days if no further
activity occurs, but it only takes a comment to keep a contribution alive
:) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- valid
- to-validate
markComment: >
This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be
closed within a week if no further activity occurs.

View File

@ -8,6 +8,7 @@
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
[https://erpnext.com](https://erpnext.com)

View File

@ -8,6 +8,16 @@ coverage:
target: auto
threshold: 0.5%
patch:
default:
target: 85%
threshold: 0%
base: auto
branches:
- develop
if_ci_failed: ignore
only_pulls: true
comment:
layout: "diff, files"
require_changes: true

1
dev-requirements.txt Normal file
View File

@ -0,0 +1 @@
hypothesis~=6.31.0

View File

@ -4,7 +4,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = '13.9.0'
__version__ = '14.0.0-dev'
def get_default_company(user=None):
'''Get default company for user'''
@ -55,9 +55,9 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable
company.save()
def encode_company_abbr(name, company):
def encode_company_abbr(name, company=None, abbr=None):
'''Returns name encoded with company abbreviation'''
company_abbr = frappe.get_cached_value('Company', company, "abbr")
company_abbr = abbr or frappe.get_cached_value('Company', company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower():

View File

@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.db.commit()
except Exception as e:
if frappe.flags.in_test:
traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(message=traceback)
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
if submit:
journal_entry.submit()
frappe.db.commit()
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(message=traceback)
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True

View File

@ -78,6 +78,7 @@ frappe.treeview_settings["Account"] = {
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {
node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
@ -175,7 +176,7 @@ frappe.treeview_settings["Account"] = {
&& node.expandable && !node.hide_add;
},
click: function() {
var me = frappe.treeview_settings['Account'].treeview;
var me = frappe.views.trees['Account'];
me.new_node();
},
btnClass: "hidden-xs"

View File

@ -1,29 +0,0 @@
QUnit.module('accounts');
QUnit.test("test account", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => frappe.set_route('Tree', 'Account'),
() => frappe.timeout(3),
() => frappe.click_button('Expand All'),
() => frappe.timeout(1),
() => frappe.click_link('Debtors'),
() => frappe.click_button('Edit'),
() => frappe.timeout(1),
() => {
assert.ok(cur_frm.doc.root_type=='Asset');
assert.ok(cur_frm.doc.report_type=='Balance Sheet');
assert.ok(cur_frm.doc.account_type=='Receivable');
},
() => frappe.click_button('Ledger'),
() => frappe.timeout(1),
() => {
// check if general ledger report shown
assert.deepEqual(frappe.get_route(), ['query-report', 'General Ledger']);
window.history.back();
return frappe.timeout(1);
},
() => done()
]);
});

View File

@ -1,69 +0,0 @@
QUnit.module('accounts');
QUnit.test("test account with number", function(assert) {
assert.expect(7);
let done = assert.async();
frappe.run_serially([
() => frappe.set_route('Tree', 'Account'),
() => frappe.click_link('Income'),
() => frappe.click_button('Add Child'),
() => frappe.timeout(.5),
() => {
cur_dialog.fields_dict.account_name.$input.val("Test Income");
cur_dialog.fields_dict.account_number.$input.val("4010");
},
() => frappe.click_button('Create New'),
() => frappe.timeout(1),
() => {
assert.ok($('a:contains("4010 - Test Income"):visible').length!=0, "Account created with number");
},
() => frappe.click_link('4010 - Test Income'),
() => frappe.click_button('Edit'),
() => frappe.timeout(.5),
() => frappe.click_button('Update Account Number'),
() => frappe.timeout(.5),
() => {
cur_dialog.fields_dict.account_number.$input.val("4020");
},
() => frappe.timeout(1),
() => cur_dialog.primary_action(),
() => frappe.timeout(1),
() => cur_frm.refresh_fields(),
() => frappe.timeout(.5),
() => {
var abbr = frappe.get_abbr(frappe.defaults.get_default("Company"));
var new_account = "4020 - Test Income - " + abbr;
assert.ok(cur_frm.doc.name==new_account, "Account renamed");
assert.ok(cur_frm.doc.account_name=="Test Income", "account name remained same");
assert.ok(cur_frm.doc.account_number=="4020", "Account number updated to 4020");
},
() => frappe.timeout(1),
() => frappe.click_button('Menu'),
() => frappe.click_link('Rename'),
() => frappe.timeout(.5),
() => {
cur_dialog.fields_dict.new_name.$input.val("4030 - Test Income");
},
() => frappe.timeout(.5),
() => frappe.click_button("Rename"),
() => frappe.timeout(2),
() => {
assert.ok(cur_frm.doc.account_name=="Test Income", "account name remained same");
assert.ok(cur_frm.doc.account_number=="4030", "Account number updated to 4030");
},
() => frappe.timeout(.5),
() => frappe.click_button('Chart of Accounts'),
() => frappe.timeout(.5),
() => frappe.click_button('Menu'),
() => frappe.click_link('Refresh'),
() => frappe.click_button('Expand All'),
() => frappe.click_link('4030 - Test Income'),
() => frappe.click_button('Delete'),
() => frappe.click_button('Yes'),
() => frappe.timeout(.5),
() => {
assert.ok($('a:contains("4030 - Test Account"):visible').length==0, "Account deleted");
},
() => done()
]);
});

View File

@ -1,46 +0,0 @@
QUnit.module('accounts');
QUnit.test("test account", assert => {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => frappe.set_route('Tree', 'Account'),
() => frappe.click_button('Expand All'),
() => frappe.click_link('Duties and Taxes - '+ frappe.get_abbr(frappe.defaults.get_default("Company"))),
() => {
if($('a:contains("CGST"):visible').length == 0){
return frappe.map_tax.make('CGST', 9);
}
},
() => {
if($('a:contains("SGST"):visible').length == 0){
return frappe.map_tax.make('SGST', 9);
}
},
() => {
if($('a:contains("IGST"):visible').length == 0){
return frappe.map_tax.make('IGST', 18);
}
},
() => {
assert.ok($('a:contains("CGST"):visible').length!=0, "CGST Checked");
assert.ok($('a:contains("SGST"):visible').length!=0, "SGST Checked");
assert.ok($('a:contains("IGST"):visible').length!=0, "IGST Checked");
},
() => done()
]);
});
frappe.map_tax = {
make:function(text,rate){
return frappe.run_serially([
() => frappe.click_button('Add Child'),
() => frappe.timeout(0.2),
() => cur_dialog.set_value('account_name',text),
() => cur_dialog.set_value('account_type','Tax'),
() => cur_dialog.set_value('tax_rate',rate),
() => cur_dialog.set_value('account_currency','INR'),
() => frappe.click_button('Create New'),
]);
}
};

View File

@ -8,7 +8,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
}
let help_content =
`<table class="table table-bordered" style="background-color: #f9f9f9;">
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<p>
<i class="fa fa-hand-right"></i>

View File

@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
def on_update(self):
@ -19,9 +21,13 @@ class AccountsSettings(Document):
frappe.db.set_default("add_taxes_from_item_tax_template",
self.get("add_taxes_from_item_tax_template", 0))
frappe.db.set_default("enable_common_party_accounting",
self.get("enable_common_party_accounting", 0))
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
self.validate_pending_reposts()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@ -53,3 +59,8 @@ class AccountsSettings(Document):
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)

View File

@ -1,35 +0,0 @@
QUnit.module('accounts');
QUnit.test("test: Accounts Settings doesn't allow negatives", function (assert) {
let done = assert.async();
assert.expect(2);
frappe.run_serially([
() => frappe.set_route('Form', 'Accounts Settings', 'Accounts Settings'),
() => frappe.timeout(2),
() => unchecked_if_checked(cur_frm, 'Allow Stale Exchange Rates', frappe.click_check),
() => cur_frm.set_value('stale_days', 0),
() => frappe.click_button('Save'),
() => frappe.timeout(2),
() => {
assert.ok(cur_dialog);
},
() => frappe.click_button('Close'),
() => cur_frm.set_value('stale_days', -1),
() => frappe.click_button('Save'),
() => frappe.timeout(2),
() => {
assert.ok(cur_dialog);
},
() => frappe.click_button('Close'),
() => done()
]);
});
const unchecked_if_checked = function(frm, field_name, fn){
if (frm.doc.allow_stale) {
return fn(field_name);
}
};

View File

@ -0,0 +1,56 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2021-11-25 10:24:39.836195",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"reference_type",
"reference_name",
"reference_detail",
"account_head",
"allocated_amount"
],
"fields": [
{
"fieldname": "reference_type",
"fieldtype": "Link",
"label": "Reference Type",
"options": "DocType"
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_type"
},
{
"fieldname": "reference_detail",
"fieldtype": "Data",
"label": "Reference Detail"
},
{
"fieldname": "account_head",
"fieldtype": "Link",
"label": "Account Head",
"options": "Account"
},
{
"fieldname": "allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount",
"options": "party_account_currency"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-11-25 10:27:51.712286",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Tax",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

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

View File

@ -25,8 +25,7 @@
"allocated_amount",
"column_break_13",
"base_tax_amount",
"base_total",
"base_allocated_amount"
"base_total"
],
"fields": [
{
@ -168,12 +167,6 @@
"label": "Allocated Amount",
"options": "currency"
},
{
"fieldname": "base_allocated_amount",
"fieldtype": "Currency",
"label": "Allocated Amount (Company Currency)",
"options": "Company:company:default_currency"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
@ -186,7 +179,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-09 11:46:58.373170",
"modified": "2021-11-25 11:10:10.945027",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Advance Taxes and Charges",

View File

@ -218,6 +218,8 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
company_account = frappe.db.get_value('Bank Account', transaction.bank_account, 'account')
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
@ -226,7 +228,7 @@ def reconcile_vouchers(bank_transaction_name, vouchers):
total_amount += get_paid_amount(frappe._dict({
'payment_document': voucher['payment_doctype'],
'payment_entry': voucher['payment_name'],
}), transaction.currency)
}), transaction.currency, company_account)
if total_amount > transaction.unallocated_amount:
frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
@ -261,7 +263,7 @@ def get_linked_payments(bank_transaction_name, document_types = None):
return matching
def check_matching(bank_account, company, transaction, document_types):
# combine all types of vocuhers
# combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
filters = {
"amount": transaction.unallocated_amount,
@ -343,13 +345,11 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query
# We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
root_type = frappe.get_value("Account", company_account, "root_type")
if root_type == "Liability":
cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
else:
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
return f"""
@ -434,7 +434,7 @@ def get_pi_matching_query(amount_condition):
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
filters={"default_account": bank_account}, fields=["parent"])]
mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
company_currency = get_company_currency(company)

View File

@ -102,7 +102,7 @@ def get_total_allocated_amount(payment_entry):
AND
bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
def get_paid_amount(payment_entry, currency):
def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
@ -115,7 +115,7 @@ def get_paid_amount(payment_entry, currency):
payment_entry.payment_entry, paid_amount_field)
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
return frappe.db.get_value('Journal Entry Account', {'parent': payment_entry.payment_entry, 'account': bank_account}, "sum(credit_in_account_currency)")
elif payment_entry.payment_document == "Expense Claim":
return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")

View File

@ -5,10 +5,10 @@
import unittest
import frappe
from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
@ -25,3 +25,29 @@ class TestFiscalYear(unittest.TestCase):
})
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
def test_record_generator():
test_records = [
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
}
]
start = 2012
end = now_datetime().year + 5
for year in range(start, end):
test_records.append({
"doctype": "Fiscal Year",
"year": f"_Test Fiscal Year {year}",
"year_start_date": f"{year}-01-01",
"year_end_date": f"{year}-12-31"
})
return test_records
test_records = test_record_generator()

View File

@ -1,69 +0,0 @@
[
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2012",
"year_end_date": "2012-12-31",
"year_start_date": "2012-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2013",
"year_end_date": "2013-12-31",
"year_start_date": "2013-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2014",
"year_end_date": "2014-12-31",
"year_start_date": "2014-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2015",
"year_end_date": "2015-12-31",
"year_start_date": "2015-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2016",
"year_end_date": "2016-12-31",
"year_start_date": "2016-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2017",
"year_end_date": "2017-12-31",
"year_start_date": "2017-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2018",
"year_end_date": "2018-12-31",
"year_start_date": "2018-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2019",
"year_end_date": "2019-12-31",
"year_start_date": "2019-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2020",
"year_end_date": "2020-12-31",
"year_start_date": "2020-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2021",
"year_end_date": "2021-12-31",
"year_start_date": "2021-01-01"
}
]

View File

@ -1,39 +0,0 @@
QUnit.module('Journal Entry');
QUnit.test("test journal entry", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Journal Entry', [
{posting_date:frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
{accounts: [
[
{'account':'Debtors - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
{'party_type':'Customer'},
{'party':'Test Customer 1'},
{'credit_in_account_currency':1000},
{'is_advance':'Yes'},
],
[
{'account':'HDFC - '+frappe.get_abbr(frappe.defaults.get_default('Company'))},
{'debit_in_account_currency':1000},
]
]},
{cheque_no:1234},
{cheque_date: frappe.datetime.add_days(frappe.datetime.nowdate(), -1)},
{user_remark: 'Test'},
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.total_debit==1000, "total debit correct");
assert.ok(cur_frm.doc.total_credit==1000, "total credit correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -6,7 +6,7 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Loyalty Program', {
setup: function(frm) {
var help_content =
`<table class="table table-bordered" style="background-color: #f9f9f9;">
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>

View File

@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
"update_stock": 0
"update_stock": 0,
"invoice_number": row.invoice_number
})
accounting_dimension = get_accounting_dimensions()
@ -200,10 +201,13 @@ def start_import(invoices):
names = []
for idx, d in enumerate(invoices):
try:
invoice_number = None
if d.invoice_number:
invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert()
doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
names.append(doc.name)

View File

@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=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)
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
doc.update(args)
return doc.make_invoices()
@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
# teardown
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
def test_renaming_of_invoice_using_invoice_number_field(self):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
#teardown
for inv in [sales_inv1, sales_inv2]:
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company)
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": args.get("invoice_number")
},
{
"qty": 2.0,
@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
"temporary_opening_account": get_temporary_opening_account(company)
"temporary_opening_account": get_temporary_opening_account(company),
"invoice_number": None
}
]
})
@ -132,7 +148,7 @@ def make_company():
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
company.country = "India"
company.country = "Pakistan"
company.insert()
return company

View File

@ -1,9 +1,11 @@
{
"actions": [],
"creation": "2017-08-29 04:26:36.159247",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"invoice_number",
"party_type",
"party",
"temporary_opening_account",
@ -103,10 +105,18 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"description": "Reference number of the invoice from the previous system",
"fieldname": "invoice_number",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Invoice Number"
}
],
"istable": 1,
"modified": "2019-07-25 15:00:00.460695",
"links": [],
"modified": "2021-12-17 19:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",

View File

@ -25,3 +25,17 @@ class PartyLink(Document):
if existing_party_link:
frappe.throw(_('{} {} is already linked with another {}')
.format(self.primary_role, self.primary_party, existing_party_link[0]))
@frappe.whitelist()
def create_party_link(primary_role, primary_party, secondary_party):
party_link = frappe.new_doc('Party Link')
party_link.primary_role = primary_role
party_link.primary_party = primary_party
party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier'
party_link.secondary_party = secondary_party
party_link.save(ignore_permissions=True)
return party_link

View File

@ -61,7 +61,6 @@
"taxes_and_charges_section",
"purchase_taxes_and_charges_template",
"sales_taxes_and_charges_template",
"advance_tax_account",
"column_break_55",
"apply_tax_withholding_amount",
"tax_withholding_category",
@ -685,15 +684,6 @@
"fieldtype": "Section Break",
"hide_border": 1
},
{
"depends_on": "eval:doc.apply_tax_withholding_amount",
"description": "Provisional tax account for advance tax. Taxes are parked in this account until payments are allocated to invoices",
"fieldname": "advance_tax_account",
"fieldtype": "Link",
"label": "Advance Tax Account",
"mandatory_depends_on": "eval:doc.apply_tax_withholding_amount",
"options": "Account"
},
{
"depends_on": "eval:doc.received_amount && doc.payment_type != 'Internal Transfer'",
"fieldname": "received_amount_after_tax",
@ -730,7 +720,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-10-22 17:50:24.632806",
"modified": "2021-11-24 18:58:24.919764",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -20,7 +20,7 @@ from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_ban
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.general_ledger import make_gl_entries, process_gl_map
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import get_account_currency, get_balance_on, get_outstanding_invoices
from erpnext.controllers.accounts_controller import (
@ -339,7 +339,7 @@ class PaymentEntry(AccountsController):
for k, v in no_oustanding_refs.items():
frappe.msgprint(
_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
.format(k, frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold("negative outstanding amount"))
.format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount")))
+ "<br><br>" + _("If this is undesirable please cancel the corresponding Payment Entry."),
title=_("Warning"), indicator="orange")
@ -433,23 +433,12 @@ class PaymentEntry(AccountsController):
if not self.apply_tax_withholding_amount:
return
if not self.advance_tax_account:
frappe.throw(_("Advance TDS account is mandatory for advance TDS deduction"))
net_total = self.paid_amount
for reference in self.get("references"):
net_total_for_tds = 0
if reference.reference_doctype == 'Purchase Order':
net_total_for_tds += flt(frappe.db.get_value('Purchase Order', reference.reference_name, 'net_total'))
if net_total_for_tds:
net_total = net_total_for_tds
# Adding args as purchase invoice to get TDS amount
args = frappe._dict({
'company': self.company,
'doctype': 'Purchase Invoice',
'doctype': 'Payment Entry',
'supplier': self.party,
'posting_date': self.posting_date,
'net_total': net_total
@ -461,7 +450,6 @@ class PaymentEntry(AccountsController):
return
tax_withholding_details.update({
'add_deduct_tax': 'Add',
'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
})
@ -623,7 +611,7 @@ class PaymentEntry(AccountsController):
if not total_negative_outstanding:
frappe.throw(_("Cannot {0} {1} {2} without any negative outstanding invoice")
.format(self.payment_type, ("to" if self.party_type=="Customer" else "from"),
.format(_(self.payment_type), (_("to") if self.party_type=="Customer" else _("from")),
self.party_type), InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
@ -689,6 +677,7 @@ class PaymentEntry(AccountsController):
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
def add_party_gl_entries(self, gl_entries):
@ -752,7 +741,8 @@ class PaymentEntry(AccountsController):
"against": self.party if self.payment_type=="Pay" else self.paid_to,
"credit_in_account_currency": self.paid_amount,
"credit": self.base_paid_amount,
"cost_center": self.cost_center
"cost_center": self.cost_center,
"post_net_value": True
}, item=self)
)
if self.payment_type in ("Receive", "Internal Transfer"):
@ -782,14 +772,10 @@ class PaymentEntry(AccountsController):
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to
payment_or_advance_account = self.get_party_account_for_taxes()
payment_account = self.get_party_account_for_taxes()
tax_amount = d.tax_amount
base_tax_amount = d.base_tax_amount
if self.advance_tax_account:
tax_amount = -1 * tax_amount
base_tax_amount = -1 * base_tax_amount
gl_entries.append(
self.get_gl_dict({
"account": d.account_head,
@ -798,19 +784,21 @@ class PaymentEntry(AccountsController):
dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
"cost_center": d.cost_center
"cost_center": d.cost_center,
"post_net_value": True,
}, account_currency, item=d))
if not d.included_in_paid_amount or self.advance_tax_account:
if not d.included_in_paid_amount:
gl_entries.append(
self.get_gl_dict({
"account": payment_or_advance_account,
"account": payment_account,
"against": against,
rev_dr_or_cr: tax_amount,
rev_dr_or_cr + "_in_account_currency": base_tax_amount
if account_currency==self.company_currency
else d.tax_amount,
"cost_center": self.cost_center,
"post_net_value": True,
}, account_currency, item=d))
def add_deductions_gl_entries(self, gl_entries):
@ -832,9 +820,7 @@ class PaymentEntry(AccountsController):
)
def get_party_account_for_taxes(self):
if self.advance_tax_account:
return self.advance_tax_account
elif self.payment_type == 'Receive':
if self.payment_type == 'Receive':
return self.paid_to
elif self.payment_type in ('Pay', 'Internal Transfer'):
return self.paid_from
@ -1106,7 +1092,7 @@ def get_outstanding_reference_documents(args):
if not data:
frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
.format(args.get("party_type").lower(), frappe.bold(args.get("party"))))
.format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))))
return data
@ -1599,13 +1585,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
})
pe.set_difference_amount()
if doc.doctype == 'Purchase Order' and doc.apply_tds:
pe.apply_tax_withholding_amount = 1
pe.tax_withholding_category = doc.tax_withholding_category
if not pe.advance_tax_account:
pe.advance_tax_account = frappe.db.get_value('Company', pe.company, 'unrealized_profit_loss_account')
return pe
def get_bank_cash_account(doc, bank_account):

View File

@ -1,55 +0,0 @@
QUnit.module('Payment Entry');
QUnit.test("test payment entry", function(assert) {
assert.expect(6);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'item_code': 'Test Product 1'},
{'qty': 1},
{'rate': 101},
]
]}
]);
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(1),
() => frappe.click_button('Make'),
() => frappe.timeout(1),
() => frappe.click_link('Payment'),
() => frappe.timeout(2),
() => {
assert.equal(frappe.get_route()[1], 'Payment Entry',
'made payment entry');
assert.equal(cur_frm.doc.party, 'Test Customer 1',
'customer set in payment entry');
assert.equal(cur_frm.doc.paid_amount, 101,
'paid amount set in payment entry');
assert.equal(cur_frm.doc.references[0].allocated_amount, 101,
'amount allocated against sales invoice');
},
() => frappe.timeout(1),
() => cur_frm.set_value('paid_amount', 100),
() => frappe.timeout(1),
() => {
frappe.model.set_value("Payment Entry Reference", cur_frm.doc.references[0].name,
"allocated_amount", 101);
},
() => frappe.timeout(1),
() => frappe.click_button('Write Off Difference Amount'),
() => frappe.timeout(1),
() => {
assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero');
assert.equal(cur_frm.doc.deductions[0].amount, 1, 'Write off amount = 1');
},
() => done()
]);
});

View File

@ -1,60 +0,0 @@
QUnit.module('Payment Entry');
QUnit.test("test payment entry", function(assert) {
assert.expect(7 );
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Purchase Invoice', [
{supplier: 'Test Supplier'},
{bill_no: 'in1234'},
{items: [
[
{'qty': 2},
{'item_code': 'Test Product 1'},
{'rate':1000},
]
]},
{update_stock:1},
{supplier_address: 'Test1-Billing'},
{contact_person: 'Contact 3-Test Supplier'},
{tc_name: 'Test Term 1'},
{terms: 'This is just a Test'}
]);
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => frappe.click_button('Make'),
() => frappe.timeout(2),
() => frappe.click_link('Payment'),
() => frappe.timeout(3),
() => cur_frm.set_value('mode_of_payment','Cash'),
() => frappe.timeout(3),
() => {
assert.equal(frappe.get_route()[1], 'Payment Entry',
'made payment entry');
assert.equal(cur_frm.doc.party, 'Test Supplier',
'supplier set in payment entry');
assert.equal(cur_frm.doc.paid_amount, 2000,
'paid amount set in payment entry');
assert.equal(cur_frm.doc.references[0].allocated_amount, 2000,
'amount allocated against purchase invoice');
assert.equal(cur_frm.doc.references[0].bill_no, 'in1234',
'invoice number allocated against purchase invoice');
assert.equal(cur_frm.get_field('total_allocated_amount').value, 2000,
'correct amount allocated in Write Off');
assert.equal(cur_frm.get_field('unallocated_amount').value, 0,
'correct amount unallocated in Write Off');
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(3),
() => done()
]);
});

View File

@ -1,28 +0,0 @@
QUnit.module('Accounts');
QUnit.test("test payment entry", function(assert) {
assert.expect(1);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Payment Entry', [
{payment_type:'Receive'},
{mode_of_payment:'Cash'},
{party_type:'Customer'},
{party:'Test Customer 3'},
{paid_amount:675},
{reference_no:123},
{reference_date: frappe.datetime.add_days(frappe.datetime.nowdate(), 0)},
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.total_allocated_amount==675, "Allocated AmountCorrect");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,67 +0,0 @@
QUnit.module('Payment Entry');
QUnit.test("test payment entry", function(assert) {
assert.expect(8);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{company: 'For Testing'},
{currency: 'INR'},
{selling_price_list: '_Test Price List'},
{items: [
[
{'qty': 1},
{'item_code': 'Test Product 1'},
]
]}
]);
},
() => frappe.timeout(1),
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1.5),
() => frappe.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.click_button('Make'),
() => frappe.timeout(1),
() => frappe.click_link('Payment'),
() => frappe.timeout(2),
() => cur_frm.set_value("paid_to", "_Test Cash - FT"),
() => frappe.timeout(0.5),
() => {
assert.equal(frappe.get_route()[1], 'Payment Entry', 'made payment entry');
assert.equal(cur_frm.doc.party, 'Test Customer 1', 'customer set in payment entry');
assert.equal(cur_frm.doc.paid_from, 'Debtors - FT', 'customer account set in payment entry');
assert.equal(cur_frm.doc.paid_amount, 100, 'paid amount set in payment entry');
assert.equal(cur_frm.doc.references[0].allocated_amount, 100,
'amount allocated against sales invoice');
},
() => cur_frm.set_value('paid_amount', 95),
() => frappe.timeout(1),
() => {
frappe.model.set_value("Payment Entry Reference",
cur_frm.doc.references[0].name, "allocated_amount", 100);
},
() => frappe.timeout(.5),
() => {
assert.equal(cur_frm.doc.difference_amount, 5, 'difference amount is 5');
},
() => {
frappe.db.set_value("Company", "For Testing", "write_off_account", "_Test Write Off - FT");
frappe.timeout(1);
frappe.db.set_value("Company", "For Testing",
"exchange_gain_loss_account", "_Test Exchange Gain/Loss - FT");
},
() => frappe.timeout(1),
() => frappe.click_button('Write Off Difference Amount'),
() => frappe.timeout(2),
() => {
assert.equal(cur_frm.doc.difference_amount, 0, 'difference amount is zero');
assert.equal(cur_frm.doc.deductions[0].amount, 5, 'Write off amount = 5');
},
() => done()
]);
});

View File

@ -548,10 +548,14 @@ def make_payment_order(source_name, target_doc=None):
return doclist
def validate_payment(doc, method=""):
if not frappe.db.has_column(doc.reference_doctype, 'status'):
def validate_payment(doc, method=None):
if doc.reference_doctype != "Payment Request" or (
frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
!= "Paid"
):
return
status = frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status')
if status == 'Paid':
frappe.throw(_("The Payment Request {0} is already paid, cannot process payment twice").format(doc.reference_docname))
frappe.throw(
_("The Payment Request {0} is already paid, cannot process payment twice")
.format(doc.reference_docname)
)

View File

@ -88,9 +88,10 @@ class PeriodClosingVoucher(AccountsController):
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict({
"account": self.closing_account_head,
"cost_center": acc.cost_center or company_cost_center,
"cost_center": cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0,

View File

@ -66,8 +66,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
company = create_company()
surplus_account = create_account()
cost_center1 = create_cost_center("Test Cost Center 1")
cost_center2 = create_cost_center("Test Cost Center 2")
cost_center1 = create_cost_center("Main")
cost_center2 = create_cost_center("Western Branch")
create_sales_invoice(
company=company,
@ -86,7 +86,10 @@ class TestPeriodClosingVoucher(unittest.TestCase):
debit_to="Debtors - TPC"
)
pcv = self.make_period_closing_voucher()
pcv = self.make_period_closing_voucher(submit=False)
pcv.cost_center_wise_pnl = 1
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head
expected_gle = (
@ -149,7 +152,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(pcv_gle, expected_gle)
def make_period_closing_voucher(self):
def make_period_closing_voucher(self, submit=True):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
pcv = frappe.get_doc({
@ -163,7 +166,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
"remarks": "test"
})
pcv.insert()
pcv.submit()
if submit:
pcv.submit()
return pcv

View File

@ -171,6 +171,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
"amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@ -1561,16 +1562,23 @@
"label": "Coupon Code",
"options": "Coupon Code",
"print_hide": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2021-08-27 20:12:57.306772",
"modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -46,6 +46,7 @@
"base_amount",
"pricing_rules",
"is_free_item",
"grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@ -800,14 +801,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-01-04 17:34:49.924531",
"modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@ -110,9 +110,15 @@ class POSInvoiceMergeLog(Document):
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
rounding_adjustment, base_rounding_adjustment = 0, 0
rounded_total, base_rounded_total = 0, 0
loyalty_amount_sum, loyalty_points_sum, idx = 0, 0, 1
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
@ -146,6 +152,8 @@ class POSInvoiceMergeLog(Document):
found = True
if not found:
tax.charge_type = 'Actual'
tax.idx = idx
idx += 1
tax.included_in_print_rate = 0
tax.tax_amount = tax.tax_amount_after_discount_amount
tax.base_tax_amount = tax.base_tax_amount_after_discount_amount
@ -163,8 +171,8 @@ class POSInvoiceMergeLog(Document):
payments.append(payment)
rounding_adjustment += doc.rounding_adjustment
rounded_total += doc.rounded_total
base_rounding_adjustment += doc.rounding_adjustment
base_rounded_total += doc.rounded_total
base_rounding_adjustment += doc.base_rounding_adjustment
base_rounded_total += doc.base_rounded_total
if loyalty_points_sum:
@ -176,9 +184,9 @@ class POSInvoiceMergeLog(Document):
invoice.set('payments', payments)
invoice.set('taxes', taxes)
invoice.set('rounding_adjustment',rounding_adjustment)
invoice.set('rounding_adjustment',base_rounding_adjustment)
invoice.set('base_rounded_total',base_rounded_total)
invoice.set('base_rounding_adjustment',base_rounding_adjustment)
invoice.set('rounded_total',rounded_total)
invoice.set('base_rounded_total',base_rounded_total)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None

View File

@ -3,22 +3,20 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.ui.form.on("POS Profile", "onload", function(frm) {
frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
frm.set_query("tc_name", function() {
return { filters: { selling: 1 } };
});
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
});
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
frm.set_query("tc_name", function() {
return { filters: { selling: 1 } };
});
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
frm.set_query("print_format", function() {
return {
filters: [
@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query("account_for_change_amount", function() {
frm.set_query("account_for_change_amount", function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
account_type: ['in', ["Cash", "Bank"]]
account_type: ['in', ["Cash", "Bank"]],
is_group: 0,
company: doc.company
}
};
});
@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', {
});
frm.set_query('company_address', function(doc) {
if(!doc.company) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query('income_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
});
frm.set_query('cost_center', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'company': doc.company,
'is_group': 0
}
};
});
frm.set_query('expense_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
});
frm.set_query("select_print_heading", function() {
return {
filters: [
['Print Heading', 'docstatus', '!=', 2]
]
};
});
frm.set_query("write_off_account", function(doc) {
return {
filters: {
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
});
frm.set_query("write_off_cost_center", function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
if(frm.doc.company) {
if (frm.doc.company) {
frm.trigger("toggle_display_account_head");
}
},
@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', {
frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company));
}
})
// Income Account
// --------------------------------
cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
};
// Cost Center
// -----------------------------
cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'company': doc.company,
'is_group': 0
}
};
};
// Expense Account
// -----------------------------
cur_frm.fields_dict["expense_account"].get_query = function(doc) {
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
};
// ------------------ Get Print Heading ------------------------------------
cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
return{
filters:[
['Print Heading', 'docstatus', '!=', 2]
]
};
};
cur_frm.fields_dict.write_off_account.get_query = function(doc) {
return{
filters:{
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
};
// Write off cost center
// -----------------------
cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
return{
filters:{
'is_group': 0,
'company': doc.company
}
};
};
});

View File

@ -38,7 +38,7 @@ frappe.ui.form.on('Pricing Rule', {
refresh: function(frm) {
var help_content =
`<table class="table table-bordered" style="background-color: #f9f9f9;">
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
<h4>
<i class="fa fa-hand-right"></i>

View File

@ -543,6 +543,75 @@ class TestPricingRule(unittest.TestCase):
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
def test_pricing_rule_for_different_currency(self):
make_item("Test Sanitizer Item")
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Sanitizer Rule",
"apply_on": "Item Code",
"items": [{
"item_code": "Test Sanitizer Item",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Rate",
"rate": 0,
"priority": 2,
"margin_type": "Percentage",
"margin_rate_or_amount": 0.0,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.rate_or_discount = 'Rate'
rule.rate = 100.0
rule.insert()
rule1 = frappe.get_doc(pricing_rule_record)
rule1.currency = 'USD'
rule1.rate_or_discount = 'Rate'
rule1.rate = 2.0
rule1.priority = 1
rule1.insert()
args = frappe._dict({
"item_code": "Test Sanitizer Item",
"company": "_Test Company",
"price_list": "_Test Price List",
"currency": "USD",
"doctype": "Sales Invoice",
"conversion_rate": 1,
"price_list_currency": "_Test Currency",
"plc_conversion_rate": 1,
"order_type": "Sales",
"customer": "_Test Customer",
"name": None,
"transaction_date": frappe.utils.nowdate()
})
details = get_item_details(args)
self.assertEqual(details.price_list_rate, 2.0)
args = frappe._dict({
"item_code": "Test Sanitizer Item",
"company": "_Test Company",
"price_list": "_Test Price List",
"currency": "INR",
"doctype": "Sales Invoice",
"conversion_rate": 1,
"price_list_currency": "_Test Currency",
"plc_conversion_rate": 1,
"order_type": "Sales",
"customer": "_Test Customer",
"name": None,
"transaction_date": frappe.utils.nowdate()
})
details = get_item_details(args)
self.assertEqual(details.price_list_rate, 100.0)
def test_pricing_rule_for_transaction(self):
make_item("Water Flask 1")
frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')

View File

@ -1,28 +0,0 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule'},
{item_code:'Test Product 2'},
{selling:1},
{applicable_for:'Customer'},
{customer:'Test Customer 3'},
{currency: frappe.defaults.get_default("currency")}
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{discount_percentage:10},
{for_price_list:'Standard Selling'}
]);
},
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 2');
assert.ok(cur_frm.doc.customer=='Test Customer 3');
},
() => done()
]);
});

View File

@ -1,58 +0,0 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule with different currency", function(assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule 2'},
{apply_on: 'Item Code'},
{item_code:'Test Product 4'},
{selling:1},
{priority: 1},
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{margin_type: 'Amount'},
{margin_rate_or_amount: 20},
{rate_or_discount: 'Rate'},
{rate:200},
{currency:'USD'}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 4');
},
() => {
return frappe.tests.make('Sales Order', [
{customer: 'Test Customer 1'},
{currency: 'INR'},
{items: [
[
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 5},
{'item_code': "Test Product 4"}
]
]}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 2', "Pricing rule correct");
// margin not applied because different currency in pricing rule
assert.ok(cur_frm.doc.items[0].margin_type==null, "Margin correct");
},
() => frappe.timeout(0.3),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,56 +0,0 @@
QUnit.module('Pricing Rule');
QUnit.test("test pricing rule with same currency", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Pricing Rule", [
{title: 'Test Pricing Rule 1'},
{apply_on: 'Item Code'},
{item_code:'Test Product 4'},
{selling:1},
{min_qty:1},
{max_qty:20},
{valid_upto: frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{rate_or_discount: 'Rate'},
{rate:200},
{currency:'USD'}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
assert.ok(cur_frm.doc.item_code=='Test Product 4');
},
() => {
return frappe.tests.make('Sales Order', [
{customer: 'Test Customer 1'},
{currency: 'USD'},
{items: [
[
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 5},
{'item_code': "Test Product 4"}
]
]}
]);
},
() => cur_frm.save(),
() => frappe.timeout(0.3),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].pricing_rule=='Test Pricing Rule 1', "Pricing rule correct");
assert.ok(cur_frm.doc.items[0].price_list_rate==200, "Item rate correct");
// get_total
assert.ok(cur_frm.doc.total== 1000, "Total correct");
},
() => frappe.timeout(0.3),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -264,6 +264,11 @@ def filter_pricing_rules(args, pricing_rules, doc=None):
else:
p.variant_of = None
if len(pricing_rules) > 1:
filtered_rules = list(filter(lambda x: x.currency==args.get('currency'), pricing_rules))
if filtered_rules:
pricing_rules = filtered_rules
# find pricing rule with highest priority
if pricing_rules:
max_priority = max(cint(p.priority) for p in pricing_rules)

View File

@ -20,6 +20,9 @@ price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discoun
product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
class TransactionExists(frappe.ValidationError):
pass
class PromotionalScheme(Document):
def validate(self):
if not self.selling and not self.buying:
@ -28,6 +31,40 @@ class PromotionalScheme(Document):
or self.product_discount_slabs):
frappe.throw(_("Price or product discount slabs are required"))
self.validate_applicable_for()
self.validate_pricing_rules()
def validate_applicable_for(self):
if self.applicable_for:
applicable_for = frappe.scrub(self.applicable_for)
if not self.get(applicable_for):
msg = (f'The field {frappe.bold(self.applicable_for)} is required')
frappe.throw(_(msg))
def validate_pricing_rules(self):
if self.is_new():
return
transaction_exists = False
docnames = []
# If user has changed applicable for
if self._doc_before_save.applicable_for == self.applicable_for:
return
docnames = frappe.get_all('Pricing Rule',
filters= {'promotional_scheme': self.name})
for docname in docnames:
if frappe.db.exists('Pricing Rule Detail',
{'pricing_rule': docname.name, 'docstatus': ('<', 2)}):
raise_for_transaction_exists(self.name)
if docnames and not transaction_exists:
for docname in docnames:
frappe.delete_doc('Pricing Rule', docname.name)
def on_update(self):
pricing_rules = frappe.get_all(
'Pricing Rule',
@ -67,6 +104,13 @@ class PromotionalScheme(Document):
{'promotional_scheme': self.name}):
frappe.delete_doc('Pricing Rule', rule.name)
def raise_for_transaction_exists(name):
msg = (f"""You can't change the {frappe.bold(_('Applicable For'))}
because transactions are present against the Promotional Scheme {frappe.bold(name)}. """)
msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.'
frappe.throw(_(msg), TransactionExists)
def get_pricing_rules(doc, rules=None):
if rules is None:
rules = {}
@ -84,45 +128,59 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
new_doc = []
args = get_args_for_pricing_rule(doc)
applicable_for = frappe.scrub(doc.get('applicable_for'))
for idx, d in enumerate(doc.get(child_doc)):
if d.name in rules:
for applicable_for_value in args.get(applicable_for):
temp_args = args.copy()
docname = frappe.get_all(
'Pricing Rule',
fields = ["promotional_scheme_id", "name", applicable_for],
filters = {
'promotional_scheme_id': d.name,
applicable_for: applicable_for_value
}
)
if docname:
pr = frappe.get_doc('Pricing Rule', docname[0].get('name'))
temp_args[applicable_for] = applicable_for_value
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
else:
pr = frappe.new_doc("Pricing Rule")
pr.title = doc.name
temp_args[applicable_for] = applicable_for_value
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
if not args.get(applicable_for):
docname = get_pricing_rule_docname(d)
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname)
new_doc.append(pr)
else:
for applicable_for_value in args.get(applicable_for):
docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value)
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
d, docname, applicable_for, applicable_for_value)
new_doc.append(pr)
else:
elif args.get(applicable_for):
applicable_for_values = args.get(applicable_for) or []
for applicable_for_value in applicable_for_values:
pr = frappe.new_doc("Pricing Rule")
pr.title = doc.name
temp_args = args.copy()
temp_args[applicable_for] = applicable_for_value
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields,
d, applicable_for=applicable_for, value= applicable_for_value)
new_doc.append(pr)
else:
pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d)
new_doc.append(pr)
return new_doc
def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str:
fields = ['promotional_scheme_id', 'name']
filters = {
'promotional_scheme_id': row.name
}
if applicable_for:
fields.append(applicable_for)
filters[applicable_for] = applicable_for_value
docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters)
return docname[0].name if docname else ''
def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None):
if docname:
pr = frappe.get_doc("Pricing Rule", docname)
else:
pr = frappe.new_doc("Pricing Rule")
pr.title = doc.name
temp_args = args.copy()
if value:
temp_args[applicable_for] = value
return set_args(temp_args, pr, doc, child_doc, discount_fields, d)
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
pr.update(args)
@ -145,6 +203,7 @@ def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
apply_on: d.get(apply_on),
'uom': d.uom
})
return pr
def get_args_for_pricing_rule(doc):

View File

@ -5,10 +5,17 @@ import unittest
import frappe
from erpnext.accounts.doctype.promotional_scheme.promotional_scheme import TransactionExists
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
class TestPromotionalScheme(unittest.TestCase):
def setUp(self):
if frappe.db.exists('Promotional Scheme', '_Test Scheme'):
frappe.delete_doc('Promotional Scheme', '_Test Scheme')
def test_promotional_scheme(self):
ps = make_promotional_scheme()
ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer')
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules),1)
@ -39,22 +46,62 @@ class TestPromotionalScheme(unittest.TestCase):
filters = {'promotional_scheme': ps.name})
self.assertEqual(price_rules, [])
def make_promotional_scheme():
def test_promotional_scheme_without_applicable_for(self):
ps = make_promotional_scheme()
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules), 1)
frappe.delete_doc('Promotional Scheme', ps.name)
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
self.assertEqual(price_rules, [])
def test_change_applicable_for_in_promotional_scheme(self):
ps = make_promotional_scheme()
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules), 1)
so = make_sales_order(qty=5, currency='USD', do_not_save=True)
so.set_missing_values()
so.save()
self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule)
ps.applicable_for = 'Customer'
ps.append('customer', {
'customer': '_Test Customer'
})
self.assertRaises(TransactionExists, ps.save)
frappe.delete_doc('Sales Order', so.name)
frappe.delete_doc('Promotional Scheme', ps.name)
price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name})
self.assertEqual(price_rules, [])
def make_promotional_scheme(**args):
args = frappe._dict(args)
ps = frappe.new_doc('Promotional Scheme')
ps.name = '_Test Scheme'
ps.append('items',{
'item_code': '_Test Item'
})
ps.selling = 1
ps.append('price_discount_slabs',{
'min_qty': 4,
'validate_applied_rule': 0,
'discount_percentage': 20,
'rule_description': 'Test'
})
ps.applicable_for = 'Customer'
ps.append('customer',{
'customer': "_Test Customer"
})
ps.company = '_Test Company'
if args.applicable_for:
ps.applicable_for = args.applicable_for
ps.append(frappe.scrub(args.applicable_for), {
frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))
})
ps.save()
return ps

View File

@ -136,7 +136,7 @@
"label": "Threshold for Suggestion"
},
{
"default": "1",
"default": "0",
"fieldname": "validate_applied_rule",
"fieldtype": "Check",
"label": "Validate Applied Rule"
@ -169,7 +169,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-19 15:49:29.598727",
"modified": "2021-11-16 00:25:33.843996",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme Price Discount",

View File

@ -130,6 +130,7 @@
"allocate_advances_automatically",
"get_advances",
"advances",
"advance_tax",
"payment_schedule_section",
"payment_terms_template",
"ignore_default_payment_terms_template",
@ -1408,13 +1409,21 @@
{
"fieldname": "column_break_147",
"fieldtype": "Column Break"
},
{
"fieldname": "advance_tax",
"fieldtype": "Table",
"hidden": 1,
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-10-12 20:55:16.145651",
"modified": "2021-11-25 13:31:02.716727",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController):
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@ -294,8 +297,15 @@ class PurchaseInvoice(BuyingController):
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company)
if not asset_category_account:
form_link = get_link_to_form('Asset Category', asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account")
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:
@ -427,6 +437,7 @@ class PurchaseInvoice(BuyingController):
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.update_advance_tax_references()
self.process_common_party_accounting()
@ -472,8 +483,6 @@ class PurchaseInvoice(BuyingController):
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
@ -729,7 +738,7 @@ class PurchaseInvoice(BuyingController):
"account": self.stock_received_but_not_billed,
"against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or "Accounting Entry for Stock",
"remarks": self.remarks or _("Accounting Entry for Stock"),
"cost_center": self.cost_center,
"project": item.project or self.project
}, item=item)
@ -937,7 +946,7 @@ class PurchaseInvoice(BuyingController):
"cost_center": tax.cost_center,
"against": self.supplier,
"credit": valuation_tax[tax.name],
"remarks": self.remarks or "Accounting Entry for Stock"
"remarks": self.remarks or _("Accounting Entry for Stock")
}, item=tax))
@property
@ -1074,6 +1083,7 @@ class PurchaseInvoice(BuyingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.update_advance_tax_references(cancel=1)
def update_project(self):
project_list = []
@ -1150,7 +1160,10 @@ class PurchaseInvoice(BuyingController):
if not self.tax_withholding_category:
return
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category)
# Adjust TDS paid on advances
self.allocate_advance_tds(tax_withholding_details, advance_taxes)
if not tax_withholding_details:
return
@ -1174,6 +1187,39 @@ class PurchaseInvoice(BuyingController):
# calculate totals again after applying TDS
self.calculate_taxes_and_totals()
def allocate_advance_tds(self, tax_withholding_details, advance_taxes):
self.set('advance_tax', [])
for tax in advance_taxes:
allocated_amount = 0
pending_amount = flt(tax.tax_amount - tax.allocated_amount)
if flt(tax_withholding_details.get('tax_amount')) >= pending_amount:
tax_withholding_details['tax_amount'] -= pending_amount
allocated_amount = pending_amount
elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount:
allocated_amount = tax_withholding_details['tax_amount']
tax_withholding_details['tax_amount'] = 0
self.append('advance_tax', {
'reference_type': 'Payment Entry',
'reference_name': tax.parent,
'reference_detail': tax.name,
'account_head': tax.account_head,
'allocated_amount': allocated_amount
})
def update_advance_tax_references(self, cancel=0):
for tax in self.get('advance_tax'):
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
if cancel:
frappe.qb.update(at).set(
at.allocated_amount, at.allocated_amount - tax.allocated_amount
).where(at.name == tax.reference_detail).run()
else:
frappe.qb.update(at).set(
at.allocated_amount, at.allocated_amount + tax.allocated_amount
).where(at.name == tax.reference_detail).run()
def set_status(self, update=False, status=None, update_modified=True):
if self.is_new():
if self.get('amended_from'):

View File

@ -1,74 +0,0 @@
QUnit.module('Purchase Invoice');
QUnit.test("test purchase invoice", function(assert) {
assert.expect(9);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Purchase Invoice', [
{supplier: 'Test Supplier'},
{bill_no: 'in123'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
{'rate':100},
]
]},
{update_stock:1},
{supplier_address: 'Test1-Billing'},
{contact_person: 'Contact 3-Test Supplier'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'},
{payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
},
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_terms_schedule': ''}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_schedule': []}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(!cur_dialog, 'Message is not shown');
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(1),
() => done()
]);
});

View File

@ -13,6 +13,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item
@ -35,6 +36,27 @@ class TestPurchaseInvoice(unittest.TestCase):
def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0)
def test_purchase_invoice_received_qty(self):
"""
1. Test if received qty is validated against accepted + rejected
2. Test if received qty is auto set on save
"""
pi = make_purchase_invoice(
qty=1,
rejected_qty=1,
received_qty=3,
item_code="_Test Item Home Desktop 200",
rejected_warehouse = "_Test Rejected Warehouse - _TC",
update_stock=True, do_not_save=True)
self.assertRaises(QtyMismatchError, pi.save)
pi.items[0].received_qty = 0
pi.save()
self.assertEqual(pi.items[0].received_qty, 2)
# teardown
pi.delete()
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
pi = frappe.copy_doc(test_records[0])
@ -811,29 +833,12 @@ class TestPurchaseInvoice(unittest.TestCase):
pi.shipping_rule = shipping_rule.name
pi.insert()
shipping_amount = 0.0
for condition in shipping_rule.get("conditions"):
if not condition.to_value or (flt(condition.from_value) <= pi.net_total <= flt(condition.to_value)):
shipping_amount = condition.shipping_amount
shipping_charge = {
"doctype": "Purchase Taxes and Charges",
"category": "Valuation and Total",
"charge_type": "Actual",
"account_head": shipping_rule.account,
"cost_center": shipping_rule.cost_center,
"tax_amount": shipping_amount,
"description": shipping_rule.name,
"add_deduct_tax": "Add"
}
pi.append("taxes", shipping_charge)
pi.save()
self.assertEqual(pi.net_total, 1250)
self.assertEqual(pi.total_taxes_and_charges, 462.3)
self.assertEqual(pi.grand_total, 1712.3)
self.assertEqual(pi.total_taxes_and_charges, 354.1)
self.assertEqual(pi.grand_total, 1604.1)
def test_make_pi_without_terms(self):
pi = make_purchase_invoice(do_not_save=1)
@ -981,7 +986,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True)
pi.set_posting_time = 1
pi.posting_date = '2019-03-15'
pi.posting_date = '2019-01-10'
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2019-01-10"
pi.items[0].service_end_date = "2019-03-15"
@ -1155,25 +1160,21 @@ class TestPurchaseInvoice(unittest.TestCase):
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
posting_date='2021-09-15')
po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
po.submit()
# Update Unrealized Profit / Loss Account which is used as default advance tax account
frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
# Create Payment Entry Against the order
payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
payment_entry.paid_from = 'Cash - _TC'
payment_entry.apply_tax_withholding_amount = 1
payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
payment_entry.save()
payment_entry.submit()
# Check GLE for Payment Entry
expected_gle = [
['_Test Account Excise Duty - _TC', 3000, 0],
['Cash - _TC', 0, 27000],
['Creditors - _TC', 27000, 0],
['Creditors - _TC', 30000, 0],
['TDS Payable - _TC', 0, 3000],
]
@ -1199,9 +1200,7 @@ class TestPurchaseInvoice(unittest.TestCase):
# Zero net effect on final TDS Payable on invoice
expected_gle = [
['_Test Account Cost for Goods Sold - _TC', 30000],
['_Test Account Excise Duty - _TC', -3000],
['Creditors - _TC', -27000],
['TDS Payable - _TC', 0]
['Creditors - _TC', -30000]
]
gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
@ -1214,6 +1213,14 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.amount)
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 3000)
purchase_invoice.cancel()
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`

View File

@ -22,10 +22,10 @@
"received_qty",
"qty",
"rejected_qty",
"stock_uom",
"col_break2",
"uom",
"conversion_factor",
"stock_uom",
"stock_qty",
"sec_break1",
"price_list_rate",
@ -175,7 +175,8 @@
{
"fieldname": "received_qty",
"fieldtype": "Float",
"label": "Received Qty"
"label": "Received Qty",
"read_only": 1
},
{
"bold": 1,
@ -223,7 +224,7 @@
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Stock Qty",
"label": "Accepted Qty in Stock UOM",
"print_hide": 1,
"read_only": 1,
"reqd": 1
@ -870,10 +871,11 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-09-01 16:04:03.538643",
"modified": "2021-11-15 17:04:07.191013",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@ -1,28 +0,0 @@
QUnit.module('Sales Taxes and Charges Template');
QUnit.test("test sales taxes and charges template", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Purchase Taxes and Charges Template', [
{title: "TEST In State GST"},
{taxes:[
[
{charge_type:"On Net Total"},
{account_head:"CGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
],
[
{charge_type:"On Net Total"},
{account_head:"SGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
]
]}
]);
},
() => {
assert.ok(cur_frm.doc.title=='TEST In State GST');
assert.ok(cur_frm.doc.name=='TEST In State GST - FT');
},
() => done()
]);
});

View File

@ -516,15 +516,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
}
}
// project name
//--------------------------
cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) {
return{
query: "erpnext.controllers.queries.get_project_name",
filters: {'customer': doc.customer}
}
}
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {
@ -978,7 +969,7 @@ frappe.ui.form.on('Sales Invoice', {
}
if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', 'Adjustment Against');
frm.set_df_property('return_against', 'label', __('Adjustment Against'));
}
if (frappe.boot.active_domains.includes("Healthcare")) {
@ -988,10 +979,10 @@ frappe.ui.form.on('Sales Invoice', {
if (cint(frm.doc.docstatus==0) && cur_frm.page.current_view_name!=="pos" && !frm.doc.is_return) {
frm.add_custom_button(__('Healthcare Services'), function() {
get_healthcare_services_to_invoice(frm);
},"Get Items From");
},__("Get Items From"));
frm.add_custom_button(__('Prescriptions'), function() {
get_drugs_to_invoice(frm);
},"Get Items From");
},__("Get Items From"));
}
}
else {

View File

@ -182,6 +182,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
"amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@ -2019,6 +2020,12 @@
"label": "Total Billing Hours",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
}
],
"icon": "fa fa-file-text",
@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-10-11 20:19:38.667508",
"modified": "2021-10-21 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
@ -2086,4 +2093,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
}
}

View File

@ -155,6 +155,8 @@ class SalesInvoice(SellingController):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
@ -842,8 +844,6 @@ class SalesInvoice(SellingController):
self.make_exchange_gain_loss_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
self.allocate_advance_taxes(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_discount_gl_entries(gl_entries)
@ -1049,6 +1049,8 @@ class SalesInvoice(SellingController):
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
asset.finance_books[0].value_after_depreciation += depreciation_amount
asset.save()
def get_posting_date_of_sales_invoice(self):
@ -1071,6 +1073,12 @@ class SalesInvoice(SellingController):
return False
def get_depreciation_amount_in_je(self, journal_entry):
if journal_entry.accounts[0].debit_in_account_currency:
return journal_entry.accounts[0].debit_in_account_currency
else:
return journal_entry.accounts[0].credit_in_account_currency
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):

View File

@ -1,73 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice", function(assert) {
assert.expect(9);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'},
{payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grand Total correct");
assert.ok(cur_frm.doc.payment_terms_template, "Payment Terms Template is correct");
assert.ok(cur_frm.doc.payment_schedule.length > 0, "Payment Term Schedule is not empty");
},
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_terms_schedule': ''}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(cur_dialog && cur_dialog.is_visible, 'Message is displayed to user');
},
() => frappe.timeout(1),
() => frappe.tests.click_button('Close'),
() => frappe.timeout(0.5),
() => frappe.tests.set_form_values(cur_frm, [{'payment_schedule': []}]),
() => {
let date = cur_frm.doc.due_date;
frappe.tests.set_control('due_date', frappe.datetime.add_days(date, 1));
frappe.timeout(0.5);
assert.ok(!cur_dialog, 'Message is not shown');
},
() => cur_frm.save(),
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1603,28 +1603,12 @@ class TestSalesInvoice(unittest.TestCase):
si.shipping_rule = shipping_rule.name
si.insert()
shipping_amount = 0.0
for condition in shipping_rule.get("conditions"):
if not condition.to_value or (flt(condition.from_value) <= si.net_total <= flt(condition.to_value)):
shipping_amount = condition.shipping_amount
shipping_charge = {
"doctype": "Sales Taxes and Charges",
"category": "Valuation and Total",
"charge_type": "Actual",
"account_head": shipping_rule.account,
"cost_center": shipping_rule.cost_center,
"tax_amount": shipping_amount,
"description": shipping_rule.name
}
si.append("taxes", shipping_charge)
si.save()
self.assertEqual(si.net_total, 1250)
self.assertEqual(si.total_taxes_and_charges, 577.05)
self.assertEqual(si.grand_total, 1827.05)
self.assertEqual(si.total_taxes_and_charges, 468.85)
self.assertEqual(si.grand_total, 1718.85)
@ -2316,6 +2300,7 @@ class TestSalesInvoice(unittest.TestCase):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
# create a customer
@ -2324,13 +2309,7 @@ class TestSalesInvoice(unittest.TestCase):
supplier = create_supplier(supplier_name="_Test Common Supplier").name
# create a party link between customer & supplier
# set primary role as supplier
party_link = frappe.new_doc("Party Link")
party_link.primary_role = "Supplier"
party_link.primary_party = supplier
party_link.secondary_role = "Customer"
party_link.secondary_party = customer
party_link.save()
party_link = create_party_link("Supplier", supplier, customer)
# enable common party accounting
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1)
@ -2406,6 +2385,29 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
item = copy.deepcopy(si.get('items')[0])
item.update({
"qty": 1,
"rate": 500,
"grant_commission": 1
})
si.append("items", item)
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
si.commission_rate = commission_rate
si.save()
self.assertEqual(si.amount_eligible_for_commission, 500)
self.assertEqual(si.total_commission, total_commission)
# Test invalid values
for commission_rate in (101, -1):
si.reload()
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
@ -2418,6 +2420,32 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def test_over_billing_case_against_delivery_note(self):
'''
Test a case where duplicating the item with qty = 1 in the invoice
allows overbilling even if it is disabled
'''
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance')
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0)
dn = create_delivery_note()
dn.submit()
si = make_sales_invoice(dn.name)
# make a copy of first item and add it to invoice
item_copy = frappe.copy_doc(si.items[0])
si.append('items', item_copy)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
si.submit()
self.assertTrue("cannot overbill" in str(err.exception).lower())
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@ -1,42 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,35 +0,0 @@
QUnit.module('Accounts');
QUnit.test("test sales invoice with margin", function(assert) {
assert.expect(3);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{selling_price_list: 'Test-Selling-USD'},
{currency: 'USD'},
{items: [
[
{'item_code': 'Test Product 4'},
{'delivery_date': frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)},
{'qty': 1},
{'margin_type': 'Percentage'},
{'margin_rate_or_amount': 20}
]
]}
]);
},
() => cur_frm.save(),
() => {
assert.ok(cur_frm.doc.items[0].rate_with_margin == 240, "Margin rate correct");
assert.ok(cur_frm.doc.items[0].base_rate_with_margin == cur_frm.doc.conversion_rate * 240, "Base margin rate correct");
assert.ok(cur_frm.doc.total == 240, "Amount correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -1,56 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice with payment", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'},
{payment_terms_template: '_Test Payment Term Template UI'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(2),
() => frappe.tests.click_button('Close'),
() => frappe.tests.click_button('Make'),
() => frappe.tests.click_link('Payment'),
() => frappe.timeout(0.2),
() => { cur_frm.set_value('mode_of_payment','Cash');},
() => { cur_frm.set_value('paid_to','Cash - '+frappe.get_abbr(frappe.defaults.get_default('Company')));},
() => {cur_frm.set_value('reference_no','TEST1234');},
() => {cur_frm.set_value('reference_date',frappe.datetime.add_days(frappe.datetime.nowdate(), 0));},
() => cur_frm.save(),
() => {
// get payment details
assert.ok(cur_frm.doc.paid_amount==590, "Paid Amount Correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => done()
]);
});

View File

@ -1,51 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice with payment request", function(assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 5},
{'item_code': 'Test Product 1'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 1', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==590, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(2),
() => frappe.tests.click_button('Close'),
() => frappe.tests.click_button('Make'),
() => frappe.tests.click_link('Payment Request'),
() => frappe.timeout(0.2),
() => { cur_frm.set_value('print_format','GST Tax Invoice');},
() => { cur_frm.set_value('email_to','test@gmail.com');},
() => cur_frm.save(),
() => {
// get payment details
assert.ok(cur_frm.doc.grand_total==590, "grand total Correct");
},
() => done()
]);
});

View File

@ -1,44 +0,0 @@
QUnit.module('Sales Invoice');
QUnit.test("test sales Invoice with serialize item", function(assert) {
assert.expect(5);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Invoice', [
{customer: 'Test Customer 1'},
{items: [
[
{'qty': 2},
{'item_code': 'Test Product 4'},
]
]},
{update_stock:1},
{customer_address: 'Test1-Billing'},
{shipping_address_name: 'Test1-Shipping'},
{contact_person: 'Contact 1-Test Customer 1'},
{taxes_and_charges: 'TEST In State GST - FT'},
{tc_name: 'Test Term 1'},
{terms: 'This is Test'}
]);
},
() => cur_frm.save(),
() => {
// get_item_details
assert.ok(cur_frm.doc.items[0].item_name=='Test Product 4', "Item name correct");
// get tax details
assert.ok(cur_frm.doc.taxes_and_charges=='TEST In State GST - FT', "Tax details correct");
// get tax account head details
assert.ok(cur_frm.doc.taxes[0].account_head=='CGST - '+frappe.get_abbr(frappe.defaults.get_default('Company')), " Account Head abbr correct");
// get batch number
assert.ok(cur_frm.doc.items[0].batch_no=='TEST-BATCH-001', " Batch Details correct");
// grand_total Calculated
assert.ok(cur_frm.doc.grand_total==218, "Grad Total correct");
},
() => frappe.tests.click_button('Submit'),
() => frappe.tests.click_button('Yes'),
() => frappe.timeout(0.3),
() => done()
]);
});

View File

@ -47,6 +47,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@ -828,15 +829,23 @@
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-08-19 13:41:53.435827",
"modified": "2021-10-05 12:24:54.968907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@ -1,28 +0,0 @@
QUnit.module('Sales Taxes and Charges Template');
QUnit.test("test sales taxes and charges template", function(assert) {
assert.expect(2);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make('Sales Taxes and Charges Template', [
{title: "TEST In State GST"},
{taxes:[
[
{charge_type:"On Net Total"},
{account_head:"CGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
],
[
{charge_type:"On Net Total"},
{account_head:"SGST - "+frappe.get_abbr(frappe.defaults.get_default("Company")) }
]
]}
]);
},
() => {
assert.ok(cur_frm.doc.title=='TEST In State GST');
assert.ok(cur_frm.doc.name=='TEST In State GST - FT');
},
() => done()
]);
});

View File

@ -1,36 +0,0 @@
QUnit.module('Shipping Rule');
QUnit.test("test Shipping Rule", function(assert) {
assert.expect(1);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Shipping Rule", [
{label: "Next Day Shipping"},
{shipping_rule_type: "Selling"},
{calculate_based_on: 'Net Total'},
{conditions:[
[
{from_value:1},
{to_value:200},
{shipping_amount:100}
],
[
{from_value:201},
{to_value:2000},
{shipping_amount:50}
],
]},
{countries:[
[
{country:'India'}
]
]},
{account:'Accounts Payable - '+frappe.get_abbr(frappe.defaults.get_default("Company"))},
{cost_center:'Main - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
]);
},
() => {assert.ok(cur_frm.doc.name=='Next Day Shipping');},
() => done()
]);
});

View File

@ -1,36 +0,0 @@
QUnit.module('Shipping Rule');
QUnit.test("test Shipping Rule", function(assert) {
assert.expect(1);
let done = assert.async();
frappe.run_serially([
() => {
return frappe.tests.make("Shipping Rule", [
{label: "Two Day Shipping"},
{shipping_rule_type: "Buying"},
{fixed_shipping_amount: 0},
{conditions:[
[
{from_value:1},
{to_value:200},
{shipping_amount:100}
],
[
{from_value:201},
{to_value:3000},
{shipping_amount:200}
],
]},
{countries:[
[
{country:'India'}
]
]},
{account:'Accounts Payable - '+frappe.get_abbr(frappe.defaults.get_default("Company"))},
{cost_center:'Main - '+frappe.get_abbr(frappe.defaults.get_default("Company"))}
]);
},
() => {assert.ok(cur_frm.doc.name=='Two Day Shipping');},
() => done()
]);
});

View File

@ -23,6 +23,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.party import get_party_account_currency
class Subscription(Document):
@ -355,6 +356,9 @@ class Subscription(Document):
if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'):
invoice.apply_tds = 1
### Add party currency to invoice
invoice.currency = get_party_account_currency(self.party_type, self.party, self.company)
## Add dimensions in invoice for subscription:
accounting_dimensions = get_accounting_dimensions()

View File

@ -1,32 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Subscription", function (assert) {
assert.expect(4);
let done = assert.async();
frappe.run_serially([
// insert a new Subscription
() => {
return frappe.tests.make("Subscription", [
{reference_doctype: 'Sales Invoice'},
{reference_document: 'SINV-00004'},
{start_date: frappe.datetime.month_start()},
{end_date: frappe.datetime.month_end()},
{frequency: 'Weekly'}
]);
},
() => cur_frm.savesubmit(),
() => frappe.timeout(1),
() => frappe.click_button('Yes'),
() => frappe.timeout(2),
() => {
assert.ok(cur_frm.doc.frequency.includes("Weekly"), "Set frequency Weekly");
assert.ok(cur_frm.doc.reference_doctype.includes("Sales Invoice"), "Set base doctype Sales Invoice");
assert.equal(cur_frm.doc.docstatus, 1, "Submitted subscription");
assert.equal(cur_frm.doc.next_schedule_date,
frappe.datetime.add_days(frappe.datetime.get_today(), 7), "Set schedule date");
},
() => done()
]);
});

View File

@ -60,15 +60,38 @@ def create_plan():
plan.billing_interval_count = 3
plan.insert()
if not frappe.db.exists('Subscription Plan', '_Test Plan Multicurrency'):
plan = frappe.new_doc('Subscription Plan')
plan.plan_name = '_Test Plan Multicurrency'
plan.item = '_Test Non Stock Item'
plan.price_determination = "Fixed Rate"
plan.cost = 50
plan.currency = 'USD'
plan.billing_interval = 'Month'
plan.billing_interval_count = 1
plan.insert()
def create_parties():
if not frappe.db.exists('Supplier', '_Test Supplier'):
supplier = frappe.new_doc('Supplier')
supplier.supplier_name = '_Test Supplier'
supplier.supplier_group = 'All Supplier Groups'
supplier.insert()
if not frappe.db.exists('Customer', '_Test Subscription Customer'):
customer = frappe.new_doc('Customer')
customer.customer_name = '_Test Subscription Customer'
customer.billing_currency = 'USD'
customer.append('accounts', {
'company': '_Test Company',
'account': '_Test Receivable USD - _TC'
})
customer.insert()
class TestSubscription(unittest.TestCase):
def setUp(self):
create_plan()
create_parties()
def test_create_subscription_with_trial_with_correct_period(self):
subscription = frappe.new_doc('Subscription')
@ -637,3 +660,22 @@ class TestSubscription(unittest.TestCase):
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
def test_multicurrency_subscription(self):
subscription = frappe.new_doc('Subscription')
subscription.party_type = 'Customer'
subscription.party = '_Test Subscription Customer'
subscription.generate_invoice_at_period_start = 1
subscription.company = '_Test Company'
# select subscription start date as '2018-01-15'
subscription.start_date = '2018-01-01'
subscription.append('plans', {'plan': '_Test Plan Multicurrency', 'qty': 1})
subscription.save()
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
self.assertEqual(subscription.status, 'Unpaid')
# Check the currency of the created invoice
currency = frappe.db.get_value('Sales Invoice', subscription.invoices[0].invoice, 'currency')
self.assertEqual(currency, 'USD')

View File

@ -75,7 +75,8 @@
"fieldname": "cost",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Cost"
"label": "Cost",
"options": "currency"
},
{
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
@ -147,7 +148,7 @@
}
],
"links": [],
"modified": "2021-08-13 10:53:44.205774",
"modified": "2021-12-10 15:24:15.794477",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",

View File

@ -95,7 +95,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
.format(tax_withholding_category, inv.company, party))
tax_amount, tax_deducted = get_tax_amount(
tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount(
party_type, parties,
inv, tax_details,
posting_date, pan_no
@ -106,7 +106,10 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
else:
tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
return tax_row
if inv.doctype == 'Purchase Invoice':
return tax_row, tax_deducted_on_advances
else:
return tax_row
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
@ -194,6 +197,10 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
to_date=tax_details.to_date, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
tax_deducted_on_advances = 0
if inv.doctype == 'Purchase Invoice':
tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details)
tax_deducted = 0
if taxable_vouchers:
@ -223,7 +230,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
return tax_amount, tax_deducted
return tax_amount, tax_deducted, tax_deducted_on_advances
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
@ -281,6 +288,29 @@ def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, pa
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
def get_taxes_deducted_on_advances_allocated(inv, tax_details):
advances = [d.reference_name for d in inv.get('advances')]
tax_info = []
if advances:
pe = frappe.qb.DocType("Payment Entry").as_("pe")
at = frappe.qb.DocType("Advance Taxes and Charges").as_("at")
tax_info = frappe.qb.from_(at).inner_join(pe).on(
pe.name == at.parent
).select(
at.parent, at.name, at.tax_amount, at.allocated_amount
).where(
pe.tax_withholding_category == tax_details.get('tax_withholding_category')
).where(
at.parent.isin(advances)
).where(
at.account_head == tax_details.account_head
).run(as_dict=True)
return tax_info
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {

View File

@ -73,8 +73,28 @@ def process_gl_map(gl_map, merge_entries=True, precision=None):
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 merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()

View File

@ -68,10 +68,12 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address)
if not party_details.get("taxes_and_charges"):
party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
billing_address=party_address, shipping_address=shipping_address)
tax_template = set_taxes(party.name, party_type, posting_date, company,
customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
billing_address=party_address, shipping_address=shipping_address)
if tax_template:
party_details['taxes_and_charges'] = tax_template
if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)

View File

@ -109,7 +109,11 @@ class ReceivablePayableReport(object):
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
outstanding = 0.0
outstanding = 0.0,
invoiced_in_account_currency = 0.0,
paid_in_account_currency = 0.0,
credit_note_in_account_currency = 0.0,
outstanding_in_account_currency = 0.0
)
self.get_invoices(gle)
@ -150,21 +154,28 @@ class ReceivablePayableReport(object):
# gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle)
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
if gle_balance > 0:
if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
# debit against sales / purchase invoice
row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
else:
# invoice
row.invoiced += gle_balance
row.invoiced_in_account_currency += gle_balance_in_account_currency
else:
# payment or credit note for receivables
if self.is_invoice(gle):
# stand alone debit / credit note
row.credit_note -= gle_balance
row.credit_note_in_account_currency -= gle_balance_in_account_currency
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
if gle.cost_center:
row.cost_center = str(gle.cost_center)
@ -216,8 +227,13 @@ class ReceivablePayableReport(object):
# as we can use this to filter out invoices without outstanding
for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
row.credit_note_in_account_currency, self.currency_precision)
row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
(abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
# non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms:
@ -529,7 +545,9 @@ class ReceivablePayableReport(object):
def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date":
entry_date = row.due_date
# use posting date as a fallback for advances posted via journal and payment entry
# when ageing viewed by due date
entry_date = row.due_date or row.posting_date
elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date
else:
@ -583,12 +601,14 @@ class ReceivablePayableReport(object):
else:
select_fields = "debit, credit"
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0} {remarks}
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from
`tabGL Entry`
where
@ -596,8 +616,8 @@ class ReceivablePayableReport(object):
and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
{2} {3} {4}"""
.format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@ -718,6 +738,13 @@ class ReceivablePayableReport(object):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
def get_gle_balance_in_account_currency(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
def get_reverse_balance_in_account_currency(self, gle):
return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')

View File

@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "show_zero_values",
"label": __("Show zero values"),
"fieldtype": "Check"
}
],
"formatter": function(value, row, column, data, default_formatter) {

View File

@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import (
get_cash_flow_accounts,
)
from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary
from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts
from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
get_fiscal_year_data,
sort_accounts,
)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_chart_data as get_pl_chart_data,
)
@ -265,7 +269,7 @@ def get_columns(companies, filters):
return columns
def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False):
accounts, accounts_by_name = get_account_heads(root_type,
accounts, accounts_by_name, parent_children_map = get_account_heads(root_type,
companies, filters)
if not accounts: return []
@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@ -364,13 +370,13 @@ def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
if not accounts:
return None, None
return None, None, None
accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name
return accounts, accounts_by_name, parent_children_map
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.

View File

@ -0,0 +1,114 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
function get_filters() {
let filters = [
{
"fieldname":"company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
"default": frappe.defaults.get_user_default("Company"),
"reqd": 1
},
{
"fieldname":"filter_based_on",
"label": __("Filter Based On"),
"fieldtype": "Select",
"options": ["Fiscal Year", "Date Range"],
"default": ["Fiscal Year"],
"reqd": 1,
on_change: function() {
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
frappe.query_report.refresh();
}
},
{
"fieldname":"period_start_date",
"label": __("Start Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"period_end_date",
"label": __("End Date"),
"fieldtype": "Date",
"hidden": 1,
"reqd": 1
},
{
"fieldname":"from_fiscal_year",
"label": __("Start Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname":"to_fiscal_year",
"label": __("End Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"),
"reqd": 1
},
{
"fieldname": "periodicity",
"label": __("Periodicity"),
"fieldtype": "Select",
"options": [
{ "value": "Monthly", "label": __("Monthly") },
{ "value": "Quarterly", "label": __("Quarterly") },
{ "value": "Half-Yearly", "label": __("Half-Yearly") },
{ "value": "Yearly", "label": __("Yearly") }
],
"default": "Monthly",
"reqd": 1
},
{
"fieldname": "type",
"label": __("Invoice Type"),
"fieldtype": "Select",
"options": [
{ "value": "Revenue", "label": __("Revenue") },
{ "value": "Expense", "label": __("Expense") }
],
"default": "Revenue",
"reqd": 1
},
{
"fieldname" : "with_upcoming_postings",
"label": __("Show with upcoming revenue/expense"),
"fieldtype": "Check",
"default": 1
}
]
return filters;
}
frappe.query_reports["Deferred Revenue and Expense"] = {
"filters": get_filters(),
"formatter": function(value, row, column, data, default_formatter){
return default_formatter(value, row, column, data);
},
onload: function(report){
let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
frappe.query_report.set_filter_value({
period_start_date: fy.year_start_date,
period_end_date: fy.year_end_date
});
});
}
};

View File

@ -0,0 +1,32 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-12-10 19:27:14.654220",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-12-10 19:27:14.654220",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Deferred Revenue and Expense",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "GL Entry",
"report_name": "Deferred Revenue and Expense",
"report_type": "Script Report",
"roles": [
{
"role": "Accounts User"
},
{
"role": "Accounts Manager"
},
{
"role": "Auditor"
}
]
}

View File

@ -0,0 +1,440 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
import frappe
from frappe import _, qb
from frappe.query_builder import Column, functions
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
from erpnext.accounts.report.financial_statements import get_period_list
class Deferred_Item(object):
"""
Helper class for processing items with deferred revenue/expense
"""
def __init__(self, item, inv, gle_entries):
self.name = item
self.parent = inv.name
self.item_name = gle_entries[0].item_name
self.service_start_date = gle_entries[0].service_start_date
self.service_end_date = gle_entries[0].service_end_date
self.base_net_amount = gle_entries[0].base_net_amount
self.filters = inv.filters
self.period_list = inv.period_list
if gle_entries[0].deferred_revenue_account:
self.type = "Deferred Sale Item"
self.deferred_account = gle_entries[0].deferred_revenue_account
elif gle_entries[0].deferred_expense_account:
self.type = "Deferred Purchase Item"
self.deferred_account = gle_entries[0].deferred_expense_account
self.gle_entries = []
# holds period wise total for item
self.period_total = []
self.last_entry_date = self.service_start_date
if gle_entries:
self.gle_entries = gle_entries
for x in self.gle_entries:
if self.get_amount(x):
self.last_entry_date = x.gle_posting_date
def report_data(self):
"""
Generate report data for output
"""
ret_data = frappe._dict({"name": self.item_name})
for period in self.period_total:
ret_data[period.key] = period.total
ret_data.indent = 1
return ret_data
def get_amount(self, entry):
"""
For a given GL/Journal posting, get balance based on item type
"""
if self.type == "Deferred Sale Item":
return entry.debit - entry.credit
elif self.type == "Deferred Purchase Item":
return -(entry.credit - entry.debit)
return 0
def get_item_total(self):
"""
Helper method - calculate booked amount. Includes simulated postings as well
"""
total = 0
for gle_posting in self.gle_entries:
total += self.get_amount(gle_posting)
return total
def calculate_amount(self, start_date, end_date):
"""
start_date, end_date - datetime.datetime.date
return - estimated amount to post for given period
Calculated based on already booked amount and item service period
"""
total_months = (
(self.service_end_date.year - self.service_start_date.year) * 12
+ (self.service_end_date.month - self.service_start_date.month)
+ 1
)
prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
get_last_day(self.service_end_date), get_first_day(self.service_start_date)
)
actual_months = rounded(total_months * prorate, 1)
already_booked_amount = self.get_item_total()
base_amount = self.base_net_amount / actual_months
if base_amount + already_booked_amount > self.base_net_amount:
base_amount = self.base_net_amount - already_booked_amount
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
partial_month = flt(date_diff(end_date, start_date)) / flt(
date_diff(get_last_day(end_date), get_first_day(start_date))
)
base_amount *= rounded(partial_month, 1)
return base_amount
def make_dummy_gle(self, name, date, amount):
"""
return - frappe._dict() of a dummy gle entry
"""
entry = frappe._dict(
{"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
)
if self.type == "Deferred Sale Item":
entry.debit = amount
elif self.type == "Deferred Purchase Item":
entry.credit = amount
return entry
def simulate_future_posting(self):
"""
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)
def calculate_item_revenue_expense_for_period(self):
"""
calculate item postings for each period and update period_total list
"""
for period in self.period_list:
period_sum = 0
actual = 0
for posting in self.gle_entries:
# if period.from_date <= posting.posting_date <= period.to_date:
if period.from_date <= posting.gle_posting_date <= period.to_date:
period_sum += self.get_amount(posting)
if posting.posted == "posted":
actual += self.get_amount(posting)
self.period_total.append(
frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
)
return self.period_total
class Deferred_Invoice(object):
def __init__(self, invoice, items, filters, period_list):
"""
Helper class for processing invoices with deferred revenue/expense items
invoice - string : invoice name
items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
"""
self.name = invoice
self.posting_date = items[0].posting_date
self.filters = filters
self.period_list = period_list
# holds period wise total for invoice
self.period_total = []
if items[0].deferred_revenue_account:
self.type = "Sales"
elif items[0].deferred_expense_account:
self.type = "Purchase"
self.items = []
# for each uniq items
self.uniq_items = set([x.item for x in items])
for item in self.uniq_items:
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
def calculate_invoice_revenue_expense_for_period(self):
"""
calculate deferred revenue/expense for all items in invoice
"""
# initialize period_total list for invoice
for period in self.period_list:
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
for item in self.items:
item_total = item.calculate_item_revenue_expense_for_period()
# update invoice total
for idx, period in enumerate(self.period_list, 0):
self.period_total[idx].total += item_total[idx].total
self.period_total[idx].actual += item_total[idx].actual
return self.period_total
def estimate_future(self):
"""
create dummy GL entries for upcoming months for all items in invoice
"""
[item.simulate_future_posting() for item in self.items]
def report_data(self):
"""
generate report data for invoice, includes invoice total
"""
ret_data = []
inv_total = frappe._dict({"name": self.name})
for x in self.period_total:
inv_total[x.key] = x.total
inv_total.indent = 0
ret_data.append(inv_total)
list(map(lambda item: ret_data.append(item.report_data()), self.items))
return ret_data
class Deferred_Revenue_and_Expense_Report(object):
def __init__(self, filters=None):
"""
Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
"""
# If no filters are provided, get user defaults
if not filters:
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": "Fiscal Year",
"period_start_date": fiscal_year.year_start_date,
"period_end_date": fiscal_year.year_end_date,
"from_fiscal_year": fiscal_year.year,
"to_fiscal_year": fiscal_year.year,
"periodicity": "Monthly",
"type": "Revenue",
"with_upcoming_postings": True,
}
)
else:
self.filters = frappe._dict(filters)
self.period_list = None
self.deferred_invoices = []
# holds period wise total for report
self.period_total = []
def get_period_list(self):
"""
Figure out selected period based on filters
"""
self.period_list = get_period_list(
self.filters.from_fiscal_year,
self.filters.to_fiscal_year,
self.filters.period_start_date,
self.filters.period_end_date,
self.filters.filter_based_on,
self.filters.periodicity,
company=self.filters.company,
)
def get_invoices(self):
"""
Get all sales and purchase invoices which has deferred revenue/expense items
"""
gle = qb.DocType("GL Entry")
# column doesn't have an alias option
posted = Column("posted")
if self.filters.type == "Revenue":
inv = qb.DocType("Sales Invoice")
inv_item = qb.DocType("Sales Invoice Item")
deferred_flag_field = inv_item["enable_deferred_revenue"]
deferred_account_field = inv_item["deferred_revenue_account"]
elif self.filters.type == "Expense":
inv = qb.DocType("Purchase Invoice")
inv_item = qb.DocType("Purchase Invoice Item")
deferred_flag_field = inv_item["enable_deferred_expense"]
deferred_account_field = inv_item["deferred_expense_account"]
query = (
qb.from_(inv_item)
.join(inv)
.on(inv.name == inv_item.parent)
.join(gle)
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
.select(
inv.name.as_("doc"),
inv.posting_date,
inv_item.name.as_("item"),
inv_item.item_name,
inv_item.service_start_date,
inv_item.service_end_date,
inv_item.base_net_amount,
deferred_account_field,
gle.posting_date.as_("gle_posting_date"),
functions.Sum(gle.debit).as_("debit"),
functions.Sum(gle.credit).as_("credit"),
posted,
)
.where(
(inv.docstatus == 1)
& (deferred_flag_field == 1)
& (
(
(self.period_list[0].from_date >= inv_item.service_start_date)
& (inv_item.service_end_date >= self.period_list[0].from_date)
)
| (
(inv_item.service_start_date >= self.period_list[0].from_date)
& (inv_item.service_start_date <= self.period_list[-1].to_date)
)
)
)
.groupby(inv.name, inv_item.name, gle.posting_date)
.orderby(gle.posting_date)
)
self.invoices = query.run(as_dict=True)
uniq_invoice = set([x.doc for x in self.invoices])
for inv in uniq_invoice:
self.deferred_invoices.append(
Deferred_Invoice(
inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
)
)
def estimate_future(self):
"""
For all Invoices estimate upcoming postings
"""
for x in self.deferred_invoices:
x.estimate_future()
def calculate_revenue_and_expense(self):
"""
calculate the deferred revenue/expense for all invoices
"""
# initialize period_total list for report
for period in self.period_list:
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
for inv in self.deferred_invoices:
inv_total = inv.calculate_invoice_revenue_expense_for_period()
# calculate total for whole report
for idx, period in enumerate(self.period_list, 0):
self.period_total[idx].total += inv_total[idx].total
self.period_total[idx].actual += inv_total[idx].actual
def get_columns(self):
columns = []
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
for period in self.period_list:
columns.append(
{
"label": _(period.label),
"fieldname": period.key,
"fieldtype": "Currency",
"read_only": 1,
})
return columns
def generate_report_data(self):
"""
Generate report data for all invoices. Adds total rows for revenue and expense
"""
ret = []
for inv in self.deferred_invoices:
ret += inv.report_data()
# empty row for padding
ret += [{}]
# add total row
if ret is not []:
if self.filters.type == "Revenue":
total_row = frappe._dict({"name": "Total Deferred Income"})
elif self.filters.type == "Expense":
total_row = frappe._dict({"name": "Total Deferred Expense"})
for idx, period in enumerate(self.period_list, 0):
total_row[period.key] = self.period_total[idx].total
ret.append(total_row)
return ret
def prepare_chart(self):
chart = {
"data": {
"labels": [period.label for period in self.period_list],
"datasets": [
{
"name": "Actual Posting",
"chartType": "bar",
"values": [x.actual for x in self.period_total],
}
],
},
"type": "axis-mixed",
"height": 500,
"axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
"barOptions": {"stacked": False, "spaceRatio": 0.5},
}
if self.filters.with_upcoming_postings:
chart["data"]["datasets"].append({
"name": "Expected",
"chartType": "line",
"values": [x.total for x in self.period_total]
})
return chart
def run(self, *args, **kwargs):
"""
Run report and generate data
"""
self.deferred_invoices.clear()
self.get_period_list()
self.get_invoices()
if self.filters.with_upcoming_postings:
self.estimate_future()
self.calculate_revenue_and_expense()
def execute(filters=None):
report = Deferred_Revenue_and_Expense_Report(filters=filters)
report.run()
columns = report.get_columns()
data = report.generate_report_data()
message = []
chart = report.prepare_chart()
return columns, data, message, chart

View File

@ -0,0 +1,253 @@
import unittest
import frappe
from frappe import qb
from frappe.utils import nowdate
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
Deferred_Revenue_and_Expense_Report,
)
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.stock.doctype.item.test_item import create_item
class TestDeferredRevenueAndExpense(unittest.TestCase):
@classmethod
def setUpClass(self):
clear_old_entries()
create_company()
def test_deferred_revenue(self):
# 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 = 3
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].service_start_date = "2021-05-01"
si.items[0].service_end_date = "2021-08-01"
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": 100.0, "actual": 100.0},
{"key": "jun_2021", "total": 100.0, "actual": 100.0},
{"key": "jul_2021", "total": 100.0, "actual": 100.0},
{"key": "aug_2021", "total": 0, "actual": 0},
]
self.assertEqual(report.period_total, expected)
def test_deferred_expense(self):
# created deferred expense accounts, if not found
deferred_expense_account = create_account(
account_name="Deferred Expense",
parent_account="Current Assets - _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()
supplier = create_supplier(
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
)
supplier.save()
item = create_item(
"_Test Office Desk",
is_stock_item=0,
warehouse="All Warehouses - _CD",
company="_Test Company DR",
)
item.enable_deferred_expense = 1
item.deferred_expense_account = deferred_expense_account
item.no_of_months_exp = 3
item.save()
pi = make_purchase_invoice(
item=item.name,
company="_Test Company DR",
supplier="_Test Furniture Supplier",
is_return=False,
update_stock=False,
posting_date=frappe.utils.datetime.date(2021, 5, 1),
parent_cost_center="Main - _CD",
cost_center="Main - _CD",
do_not_save=True,
rate=300,
price_list_rate=300,
warehouse="All Warehouses - _CD",
qty=1,
)
pi.set_posting_time = True
pi.items[0].enable_deferred_expense = 1
pi.items[0].service_start_date = "2021-05-01"
pi.items[0].service_end_date = "2021-08-01"
pi.items[0].deferred_expense_account = deferred_expense_account
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
pi.save()
pi.submit()
pda = frappe.get_doc(
dict(
doctype="Process Deferred Accounting",
posting_date=nowdate(),
start_date="2021-05-01",
end_date="2021-08-01",
type="Expense",
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": "Expense",
"with_upcoming_postings": False,
}
)
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
report.run()
expected = [
{"key": "may_2021", "total": -100.0, "actual": -100.0},
{"key": "jun_2021", "total": -100.0, "actual": -100.0},
{"key": "jul_2021", "total": -100.0, "actual": -100.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")
if not company:
company = frappe.new_doc("Company")
company.company_name = "_Test Company DR"
company.default_currency = "INR"
company.chart_of_accounts = "Standard"
company.insert()
def clear_old_entries():
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")
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
).run()
qb.from_(item).delete().where(
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
).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

@ -44,7 +44,7 @@ frappe.query_reports["Gross Profit"] = {
"formatter": function(value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
if (data && data.indent == 0.0) {
if (data && (data.indent == 0.0 || row[1].content == "Total")) {
value = $(`<span>${value}</span>`);
var $value = $(value).css("font-weight", "bold");
value = $value.wrap("<p></p>").parent().html();

View File

@ -9,7 +9,7 @@
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2021-08-19 18:57:07.468202",
"modified": "2021-11-13 19:14:23.730198",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Gross Profit",

View File

@ -19,7 +19,7 @@ def execute(filters=None):
data = []
group_wise_columns = frappe._dict({
"invoice": ["parent", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", \
"invoice": ["invoice_or_item", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description",
"warehouse", "qty", "base_rate", "buying_rate", "base_amount",
"buying_amount", "gross_profit", "gross_profit_percent", "project"],
"item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate",
@ -77,13 +77,15 @@ def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_
row.append(filters.currency)
if idx == len(gross_profit_data.grouped_data)-1:
row[0] = frappe.bold("Total")
row[0] = "Total"
data.append(row)
def get_columns(group_wise_columns, filters):
columns = []
column_map = frappe._dict({
"parent": _("Sales Invoice") + ":Link/Sales Invoice:120",
"invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120",
"posting_date": _("Posting Date") + ":Date:100",
"posting_time": _("Posting Time") + ":Data:100",
"item_code": _("Item Code") + ":Link/Item:100",
@ -122,7 +124,7 @@ def get_columns(group_wise_columns, filters):
def get_column_names():
return frappe._dict({
'parent': 'sales_invoice',
'invoice_or_item': 'sales_invoice',
'customer': 'customer',
'customer_group': 'customer_group',
'posting_date': 'posting_date',
@ -245,19 +247,28 @@ class GrossProfitGenerator(object):
self.add_to_totals(new_row)
else:
for i, row in enumerate(self.grouped[key]):
if row.parent in self.returned_invoices \
and row.item_code in self.returned_invoices[row.parent]:
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
row.qty += flt(returned_item_row.qty)
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if (flt(row.qty) or row.base_amount) and self.is_not_invoice_row(row):
row = self.set_average_rate(row)
self.grouped_data.append(row)
self.add_to_totals(row)
if row.indent == 1.0:
if row.parent in self.returned_invoices \
and row.item_code in self.returned_invoices[row.parent]:
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows:
row.qty += flt(returned_item_row.qty)
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if (flt(row.qty) or row.base_amount):
row = self.set_average_rate(row)
self.grouped_data.append(row)
self.add_to_totals(row)
self.set_average_gross_profit(self.totals)
self.grouped_data.append(self.totals)
if self.filters.get("group_by") == "Invoice":
self.totals.indent = 0.0
self.totals.parent_invoice = ""
self.totals.invoice_or_item = "Total"
self.si_list.append(self.totals)
else:
self.grouped_data.append(self.totals)
def is_not_invoice_row(self, row):
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice"
@ -446,7 +457,7 @@ class GrossProfitGenerator(object):
if not row.indent:
row.indent = 1.0
row.parent_invoice = row.parent
row.parent = row.item_code
row.invoice_or_item = row.item_code
if frappe.db.exists('Product Bundle', row.item_code):
self.add_bundle_items(row, index)
@ -455,7 +466,8 @@ class GrossProfitGenerator(object):
return frappe._dict({
'parent_invoice': "",
'indent': 0.0,
'parent': row.parent,
'invoice_or_item': row.parent,
'parent': None,
'posting_date': row.posting_date,
'posting_time': row.posting_time,
'project': row.project,
@ -499,7 +511,8 @@ class GrossProfitGenerator(object):
return frappe._dict({
'parent_invoice': product_bundle.item_code,
'indent': product_bundle.indent + 1,
'parent': item.item_code,
'parent': None,
'invoice_or_item': item.item_code,
'posting_date': product_bundle.posting_date,
'posting_time': product_bundle.posting_time,
'project': product_bundle.project,

View File

@ -1,27 +1,30 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"creation": "2013-05-06 12:28:23",
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"idx": 3,
"is_standard": "Yes",
"modified": "2017-03-06 05:52:57.645281",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Partners Commission",
"owner": "Administrator",
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
"ref_doctype": "Sales Invoice",
"report_name": "Sales Partners Commission",
"report_type": "Query Report",
"add_total_row": 0,
"columns": [],
"creation": "2013-05-06 12:28:23",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2021-10-06 06:26:07.881340",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Partners Commission",
"owner": "Administrator",
"prepared_report": 0,
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
"ref_doctype": "Sales Invoice",
"report_name": "Sales Partners Commission",
"report_type": "Query Report",
"roles": [
{
"role": "Accounts Manager"
},
},
{
"role": "Accounts User"
}
]
}
}

View File

@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit)
if rate and tds_deducted:
if tds_deducted:
row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name')
@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_supplier_pan_map():
supplier_map = frappe._dict()
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
for d in suppliers:
supplier_map[d.name] = d

View File

@ -1,116 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Crop", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(2);
frappe.run_serially([
// insert a new Item
() => frappe.tests.make('Item', [
// values to be set
{item_code: 'Basil Seeds'},
{item_name: 'Basil Seeds'},
{item_group: 'Seed'}
]),
// insert a new Item
() => frappe.tests.make('Item', [
// values to be set
{item_code: 'Twigs'},
{item_name: 'Twigs'},
{item_group: 'By-product'}
]),
// insert a new Item
() => frappe.tests.make('Item', [
// values to be set
{item_code: 'Basil Leaves'},
{item_name: 'Basil Leaves'},
{item_group: 'Produce'}
]),
// insert a new Crop
() => frappe.tests.make('Crop', [
// values to be set
{title: 'Basil from seed'},
{crop_name: 'Basil'},
{scientific_name: 'Ocimum basilicum'},
{materials_required: [
[
{item_code: 'Basil Seeds'},
{qty: '25'},
{uom: 'Nos'},
{rate: '1'}
],
[
{item_code: 'Urea'},
{qty: '5'},
{uom: 'Kg'},
{rate: '10'}
]
]},
{byproducts: [
[
{item_code: 'Twigs'},
{qty: '25'},
{uom: 'Nos'},
{rate: '1'}
]
]},
{produce: [
[
{item_code: 'Basil Leaves'},
{qty: '100'},
{uom: 'Nos'},
{rate: '1'}
]
]},
{agriculture_task: [
[
{task_name: "Plough the field"},
{start_day: 1},
{end_day: 1},
{holiday_management: "Ignore holidays"}
],
[
{task_name: "Plant the seeds"},
{start_day: 2},
{end_day: 3},
{holiday_management: "Ignore holidays"}
],
[
{task_name: "Water the field"},
{start_day: 4},
{end_day: 4},
{holiday_management: "Ignore holidays"}
],
[
{task_name: "First harvest"},
{start_day: 8},
{end_day: 8},
{holiday_management: "Ignore holidays"}
],
[
{task_name: "Add the fertilizer"},
{start_day: 10},
{end_day: 12},
{holiday_management: "Ignore holidays"}
],
[
{task_name: "Final cut"},
{start_day: 15},
{end_day: 15},
{holiday_management: "Ignore holidays"}
]
]}
]),
// agriculture task list
() => {
assert.equal(cur_frm.doc.name, 'Basil from seed');
assert.equal(cur_frm.doc.period, 15);
},
() => done()
]);
});

View File

@ -1,34 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Crop Cycle", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Crop Cycle
() => frappe.tests.make('Crop Cycle', [
// values to be set
{title: 'Basil from seed 2017'},
{detected_disease: [
[
{start_date: '2017-11-21'},
{disease: 'Aphids'}
]
]},
{linked_land_unit: [
[
{land_unit: 'Basil Farm'}
]
]},
{crop: 'Basil from seed'},
{start_date: '2017-11-11'},
{cycle_type: 'Less than a year'}
]),
() => assert.equal(cur_frm.doc.name, 'Basil from seed 2017'),
() => done()
]);
});

View File

@ -1,38 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Disease", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Disease
() => frappe.tests.make('Disease', [
// values to be set
{common_name: 'Aphids'},
{scientific_name: 'Aphidoidea'},
{treatment_task: [
[
{task_name: "Survey and find the aphid locations"},
{start_day: 1},
{end_day: 2},
{holiday_management: "Ignore holidays"}
],
[
{task_name: "Apply Pesticides"},
{start_day: 3},
{end_day: 3},
{holiday_management: "Ignore holidays"}
]
]}
]),
() => {
assert.equal(cur_frm.doc.treatment_period, 3);
},
() => done()
]);
});

View File

@ -1,31 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Fertilizer", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Item
() => frappe.tests.make('Item', [
// values to be set
{item_code: 'Urea'},
{item_name: 'Urea'},
{item_group: 'Fertilizer'}
]),
// insert a new Fertilizer
() => frappe.tests.make('Fertilizer', [
// values to be set
{fertilizer_name: 'Urea'},
{item: 'Urea'}
]),
() => {
assert.equal(cur_frm.doc.name, 'Urea');
},
() => done()
]);
});

View File

@ -1,26 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Soil Texture", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(2);
frappe.run_serially([
// insert a new Soil Texture
() => frappe.tests.make('Soil Texture', [
// values to be set
{location: '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[72.882185,19.076395]}}]}'},
{collection_datetime: '2017-11-08'},
{clay_composition: 20},
{sand_composition: 30}
]),
() => {
assert.equal(cur_frm.doc.silt_composition, 50);
assert.equal(cur_frm.doc.soil_type, 'Silt Loam');
},
() => done()
]);
});

View File

@ -1,25 +0,0 @@
/* eslint-disable */
// rename this file from _test_[name] to test_[name] to activate
// and remove above this line
QUnit.test("test: Water Analysis", function (assert) {
let done = assert.async();
// number of asserts
assert.expect(1);
frappe.run_serially([
// insert a new Water Analysis
() => frappe.tests.make('Water Analysis', [
// values to be set
{location: '{"type":"FeatureCollection","features":[{"type":"Feature","properties":{},"geometry":{"type":"Point","coordinates":[72.882185,19.076395]}}]}'},
{collection_datetime: '2017-11-08 18:43:57'},
{laboratory_testing_datetime: '2017-11-10 18:43:57'}
]),
() => {
assert.equal(cur_frm.doc.result_datetime, '2017-11-10 18:43:57');
},
() => done()
]);
});

View File

@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() {
frm.add_custom_button(__("Transfer Asset"), function() {
erpnext.asset.transfer_asset(frm);
}, __("Manage"));
frm.add_custom_button("Scrap Asset", function() {
frm.add_custom_button(__("Scrap Asset"), function() {
erpnext.asset.scrap_asset(frm);
}, __("Manage"));
frm.add_custom_button("Sell Asset", function() {
frm.add_custom_button(__("Sell Asset"), function() {
frm.trigger("make_sales_invoice");
}, __("Manage"));
} else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() {
frm.add_custom_button(__("Restore Asset"), function() {
erpnext.asset.restore_asset(frm);
}, __("Manage"));
}
@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment");
frm.trigger("create_asset_value_adjustment");
}, __("Manage"));
}
@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', {
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button("View General Ledger", function() {
frm.add_custom_button(__("View General Ledger"), function() {
frappe.route_options = {
"voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date,
@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
});
},
create_asset_adjustment: function(frm) {
create_asset_value_adjustment: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"asset_category": frm.doc.asset_category,
"company": frm.doc.company
},
method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment",
method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
freeze: 1,
callback: function(r) {
var doclist = frappe.model.sync(r.message);

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