Merge branch 'version-13' of https://github.com/frappe/erpnext into enterprise-hotfix

This commit is contained in:
Deepesh Garg
2022-03-11 16:55:03 +05:30
300 changed files with 8014 additions and 2550 deletions

View File

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

View File

@@ -5,9 +5,14 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
concurrency:
group: patch-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@@ -25,13 +30,18 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.6
python-version: 3.7
- name: Setup Node
uses: actions/setup-node@v2

View File

@@ -5,6 +5,7 @@ on:
paths-ignore:
- '**.js'
- '**.md'
types: [opened, unlabeled, synchronize, reopened]
workflow_dispatch:
push:
branches: [ develop ]
@@ -12,6 +13,10 @@ on:
- '**.js'
- '**.md'
concurrency:
group: server-mariadb-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04
@@ -35,6 +40,12 @@ jobs:
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Check for merge conficts label
if: ${{ contains(github.event.pull_request.labels.*.name, 'conflicts') }}
run: |
echo "Remove merge conflicts and remove conflict label to run CI"
exit 1
- name: Clone
uses: actions/checkout@v2
@@ -89,39 +100,8 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-18.04
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -6,6 +6,10 @@ on:
- '**.md'
workflow_dispatch:
concurrency:
group: ui-v13-${{ github.event.number }}
cancel-in-progress: true
jobs:
test:
runs-on: ubuntu-18.04

View File

@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
__version__ = '13.18.0'
__version__ = '13.22.1'
def get_default_company(user=None):
'''Get default company for user'''

View File

@@ -121,6 +121,7 @@ def get_booking_dates(doc, item, posting_date=None):
prev_gl_entry = frappe.db.sql('''
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
order by posting_date desc limit 1
''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@@ -228,6 +229,7 @@ def get_already_booked_amount(doc, item):
gl_entries_details = frappe.db.sql('''
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
and is_cancelled = 0
group by voucher_detail_no
'''.format(total_credit_debit, total_credit_debit_currency),
(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
@@ -255,11 +257,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return
account_currency = get_account_currency(item.expense_account)
account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
@@ -280,6 +284,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount:
return
# check if books nor frozen till endate:
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@@ -407,8 +415,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': credit_account,
'credit': base_amount,
'credit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,
@@ -421,8 +427,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': debit_account,
'debit': base_amount,
'debit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency,
'reference_name': doc.name,
'reference_type': doc.doctype,

View File

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

View File

@@ -14,6 +14,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
});
},
onload: function (frm) {
frm.trigger('bank_account');
},
refresh: function (frm) {
frappe.require("assets/js/bank-reconciliation-tool.min.js", () =>
frm.trigger("make_reconciliation_tool")
@@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
bank_account: function (frm) {
frappe.db.get_value(
"Bank Account",
frm.bank_account,
frm.doc.bank_account,
"account",
(r) => {
frappe.db.get_value(
@@ -60,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"account_currency",
(r) => {
frm.currency = r.account_currency;
frm.trigger("render_chart");
}
);
}
@@ -124,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}
},
render_chart(frm) {
render_chart: frappe.utils.debounce((frm) => {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
@@ -136,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency,
}
);
},
}, 500),
render(frm) {
if (frm.doc.bank_account) {

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2018-11-22 22:45:00.370913",
"creation": "2022-01-19 01:09:13.297137",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
@@ -10,6 +10,9 @@
"field_order": [
"title",
"company",
"column_break_3",
"disabled",
"section_break_5",
"taxes"
],
"fields": [
@@ -36,10 +39,24 @@
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
}
],
"links": [],
"modified": "2021-03-08 19:50:21.416513",
"modified": "2022-01-18 21:11:23.105589",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -82,6 +99,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@@ -8,6 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
},
refresh: function(frm) {
@@ -31,7 +32,7 @@ frappe.ui.form.on("Journal Entry", {
if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm);
}, __('Make'));
}, __('Actions'));
}
if (frm.doc.__islocal) {

View File

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

View File

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

View File

@@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
"modified": "2022-01-04 13:40:15.927675",
"modified": "2022-01-04 16:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",

View File

@@ -135,7 +135,7 @@ class OpeningInvoiceCreationTool(Document):
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty)
return frappe._dict({
item_dict = frappe._dict({
"uom": default_uom,
"rate": rate or 0.0,
"qty": row.qty,
@@ -146,6 +146,13 @@ class OpeningInvoiceCreationTool(Document):
"cost_center": cost_center
})
for dimension in get_accounting_dimensions():
item_dict.update({
dimension: row.get(dimension)
})
return item_dict
item = get_item_dict()
invoice = frappe._dict({
@@ -159,14 +166,15 @@ 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,
"invoice_number": row.invoice_number
"update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
"invoice_number": row.invoice_number,
"disable_rounded_total": 1
})
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
invoice.update({
dimension: item.get(dimension)
dimension: self.get(dimension) or item.get(dimension)
})
return invoice

View File

@@ -1,51 +1,49 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
create_dimension,
disable_dimension,
)
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Customer", "Supplier"]
test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def setUp(self):
class TestOpeningInvoiceCreationTool(ERPNextTestCase):
@classmethod
def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
create_dimension()
return super().setUpClass()
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None, department=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
party_1=party_1, party_2=party_2, invoice_number=invoice_number, department=department)
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
try:
invoices = self.make_invoices(company="_Test Opening Invoice Company")
invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
"keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"],
}
self.check_expected_values(invoices, expected_value)
self.assertEqual(len(invoices), 2)
expected_value = {
"keys": ["customer", "outstanding_amount", "status"],
0: ["_Test Customer", 300, "Overdue"],
1: ["_Test Customer 1", 250, "Overdue"],
}
self.check_expected_values(invoices, expected_value)
si = frappe.get_doc("Sales Invoice", invoices[0])
si = frappe.get_doc("Sales Invoice", invoices[0])
# Check if update stock is not enabled
self.assertEqual(si.update_stock, 0)
finally:
property_setter.delete()
clear_doctype_cache("Sales Invoice")
# Check if update stock is not enabled
self.assertEqual(si.update_stock, 0)
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -106,6 +104,19 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
doc = frappe.get_doc('Sales Invoice', inv)
doc.cancel()
def test_opening_invoice_with_accounting_dimension(self):
invoices = self.make_invoices(invoice_type="Sales", company="_Test Opening Invoice Company", department='Sales - _TOIC')
expected_value = {
"keys": ["customer", "outstanding_amount", "status", "department"],
0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
}
self.check_expected_values(invoices, expected_value, invoice_type="Sales")
def tearDown(self):
disable_dimension()
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")

View File

@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency));
if (frm.doc.payment_type == "Pay") {
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_to_account_currency != company_currency));
} else {
frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
(frm.doc.paid_from_account_currency != company_currency));
}
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -232,7 +238,8 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
"base_total_taxes_and_charges"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@@ -341,6 +348,8 @@ frappe.ui.form.on('Payment Entry', {
}
frm.set_party_account_based_on_party = true;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@@ -374,7 +383,11 @@ frappe.ui.form.on('Payment Entry', {
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
}
},
() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
frm.doc.paid_from_account_currency, company_currency),
() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@@ -478,14 +491,14 @@ frappe.ui.form.on('Payment Entry', {
},
paid_from_account_currency: function(frm) {
if(!frm.doc.paid_from_account_currency) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@@ -505,8 +518,8 @@ frappe.ui.form.on('Payment Entry', {
},
paid_to_account_currency: function(frm) {
if(!frm.doc.paid_to_account_currency) return;
var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);

View File

@@ -66,7 +66,9 @@
"tax_withholding_category",
"section_break_56",
"taxes",
"section_break_60",
"base_total_taxes_and_charges",
"column_break_61",
"total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
@@ -715,12 +717,21 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Paid To Account Type"
},
{
"fieldname": "column_break_61",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_60",
"fieldtype": "Section Break",
"hide_border": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-11-24 18:58:24.919764",
"modified": "2022-02-23 20:08:39.559814",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -763,6 +774,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@@ -3,6 +3,7 @@
import json
from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
@@ -945,8 +946,12 @@ class PaymentEntry(AccountsController):
tax.base_total = tax.total * self.source_exchange_rate
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
if self.payment_type == 'Pay':
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if self.get('taxes'):
self.paid_amount_after_tax = self.get('taxes')[-1].base_total
@@ -1077,7 +1082,7 @@ def get_outstanding_reference_documents(args):
if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
# Get all SO / PO which are not fully billed or aginst which full advance not paid
# Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
if (args.get("party_type") != "Student"):
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
@@ -1524,6 +1529,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']:
pe.project = (doc.get('project') or
reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
@@ -1709,7 +1718,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice']
has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule
if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
if term.discount_type == 'Percentage':

View File

@@ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
def test_multi_currency_payment_entry_with_taxes(self):
payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC',
save=True)
payment_entry.append('taxes', {
'account_head': '_Test Account Service Tax - _TC',
'charge_type': 'Actual',
'tax_amount': 10,
'add_deduct_tax': 'Add',
'description': 'Test'
})
payment_entry.save()
self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2))
def create_payment_entry(**args):
payment_entry = frappe.new_doc('Payment Entry')
payment_entry.company = args.get('company') or '_Test Company'
payment_entry.payment_type = args.get('payment_type') or 'Pay'
payment_entry.party_type = args.get('party_type') or 'Supplier'
payment_entry.party = args.get('party') or '_Test Supplier'
payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC'
payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC'
payment_entry.paid_amount = args.get('paid_amount') or 1000
payment_entry.setup_party_account_field()
payment_entry.set_missing_values()
payment_entry.set_exchange_rate()
payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
payment_entry.reference_no = 'Test001'
payment_entry.reference_date = nowdate()
if args.get('save'):
payment_entry.save()
if args.get('submit'):
payment_entry.submit()
return payment_entry
def create_payment_terms_template():
create_payment_term('Basic Amount Receivable')

View File

@@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
update_multi_mode_option,
)
from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -42,7 +43,6 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status()
self.set_account_for_mode_of_payment()
self.validate_pos()
@@ -125,9 +125,26 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
elif invalid_serial_nos:
frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.")
frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.")
.format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable"))
def validate_pos_reserved_batch_qty(self, item):
filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no}
available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code)
reserved_batch_qty = get_pos_reserved_batch_qty(filters)
bold_item_name = frappe.bold(item.item_name)
bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty))
bold_invalid_batch_no = frappe.bold(item.batch_no)
if (available_batch_qty - reserved_batch_qty) == 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.")
.format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable"))
elif (available_batch_qty - reserved_batch_qty - item.qty) < 0:
frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required")
.format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable"))
def validate_delivered_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
delivered_serial_nos = frappe.db.get_list('Serial No', {
@@ -141,20 +158,39 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_invalid_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
error_msg = []
invalid_serials, msg = "", ""
for serial_no in serial_nos:
if not frappe.db.exists('Serial No', serial_no):
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
msg = (_("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
if invalid_serials:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self):
if self.is_return or self.docstatus != 1:
return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item:
return
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)
self.validate_invalid_serial_nos(d)
elif d.batch_no:
self.validate_pos_reserved_batch_qty(d)
else:
if allow_negative_stock:
return
available_stock = get_stock_availability(d.item_code, d.warehouse)
available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0:
@@ -225,14 +261,6 @@ class POSInvoice(SalesInvoice):
.format(d.idx, bold_serial_no, bold_return_against)
)
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
if not frappe.db.exists('Product Bundle', d.item_code):
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self):
if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
@@ -334,7 +362,6 @@ class POSInvoice(SalesInvoice):
if not for_validate and not self.customer:
self.customer = profile.customer
self.ignore_pricing_rule = profile.ignore_pricing_rule
self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount
self.set_warehouse = profile.get('warehouse') or self.set_warehouse
@@ -412,7 +439,6 @@ class POSInvoice(SalesInvoice):
self.paid_amount = 0
def set_account_for_mode_of_payment(self):
self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default]
for pay in self.payments:
if not pay.account:
pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account")
@@ -473,12 +499,18 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty
return bin_qty - pos_sales_qty, is_stock_item
else:
is_stock_item = False
if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse)
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
return 0, is_stock_item
def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)

View File

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

View File

@@ -136,9 +136,15 @@ class POSInvoiceMergeLog(Document):
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
found = True
i.qty = i.qty + item.qty
i.amount = i.amount + item.net_amount
i.net_amount = i.amount
i.base_amount = i.base_amount + item.base_net_amount
i.base_net_amount = i.base_amount
if not found:
item.rate = item.net_rate
item.amount = item.net_amount
item.base_amount = item.base_net_amount
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
@@ -170,6 +176,7 @@ class POSInvoiceMergeLog(Document):
found = True
if not found:
payments.append(payment)
rounding_adjustment += doc.rounding_adjustment
rounded_total += doc.rounded_total
base_rounding_adjustment += doc.base_rounding_adjustment

View File

@@ -5,6 +5,7 @@ import json
import unittest
import frappe
from frappe.tests.utils import change_settings
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
@@ -12,6 +13,7 @@ from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_inv
from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import (
consolidate_pos_invoices,
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -150,3 +152,229 @@ class TestPOSInvoiceMergeLog(unittest.TestCase):
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_1(self):
'''
Test round off error in consolidated invoice creation if POS Invoice has inclusive tax
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=3, rate=10000, do_not_save=True)
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 30000
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 0)
self.assertEqual(consolidated_invoice.status, 'Paid')
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_round_off_error_2(self):
'''
Test the same case as above but with an Unpaid POS Invoice
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=6, rate=10000, do_not_save=True)
inv2.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 7.5,
"included_in_print_rate": 1
})
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60000
})
inv2.insert()
inv2.submit()
inv3 = create_pos_invoice(qty=3, rate=600, do_not_save=True)
inv3.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000
})
inv3.insert()
inv3.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.outstanding_amount, 800)
self.assertNotEqual(consolidated_invoice.status, 'Paid')
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
@change_settings("System Settings", {"number_format": "#,###.###", "currency_precision": 3, "float_precision": 3})
def test_consolidation_round_off_error_3(self):
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
item_rates = [69, 59, 29]
for i in [1, 2]:
inv = create_pos_invoice(is_return=1, do_not_save=1)
inv.items = []
for rate in item_rates:
inv.append("items", {
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC",
"qty": -1,
"rate": rate,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
inv.append("taxes", {
"account_head": "_Test Account VAT - _TC",
"charge_type": "On Net Total",
"cost_center": "_Test Cost Center - _TC",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"rate": 15,
"included_in_print_rate": 1
})
inv.payments = []
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -157
})
inv.paid_amount = -157
inv.save()
inv.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.status, 'Return')
self.assertEqual(consolidated_invoice.rounding_adjustment, -0.001)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")
def test_consolidation_rounding_adjustment(self):
'''
Test if the rounding adjustment is calculated correctly
'''
frappe.db.sql("delete from `tabPOS Invoice`")
try:
make_stock_entry(
to_warehouse="_Test Warehouse - _TC",
item_code="_Test Item",
rate=8000,
qty=10,
)
init_user_and_profile()
inv = create_pos_invoice(qty=1, rate=69.5, do_not_save=True)
inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 70
})
inv.insert()
inv.submit()
inv2 = create_pos_invoice(qty=1, rate=59.5, do_not_save=True)
inv2.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60
})
inv2.insert()
inv2.submit()
consolidate_pos_invoices()
inv.load_from_db()
consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice)
self.assertEqual(consolidated_invoice.rounding_adjustment, 1)
finally:
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
frappe.db.sql("delete from `tabPOS Invoice`")

View File

@@ -250,13 +250,17 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
"free_item_data": [],
"parent": args.parent,
"parenttype": args.parenttype,
"child_docname": args.get('child_docname')
"child_docname": args.get('child_docname'),
})
if args.ignore_pricing_rule or not args.item_code:
if frappe.db.exists(args.doctype, args.name) and args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
item_details, args.get('item_code'))
item_details = remove_pricing_rule_for_item(
args.get("pricing_rules"),
item_details,
item_code=args.get("item_code"),
rate=args.get("price_list_rate"),
)
return item_details
update_args_for_pricing_rule(args)
@@ -309,8 +313,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if not doc: return item_details
elif args.get("pricing_rules"):
item_details = remove_pricing_rule_for_item(args.get("pricing_rules"),
item_details, args.get('item_code'))
item_details = remove_pricing_rule_for_item(
args.get("pricing_rules"),
item_details,
item_code=args.get("item_code"),
rate=args.get("price_list_rate"),
)
return item_details
@@ -391,7 +399,7 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
item_details[field] += (pricing_rule.get(field, 0)
if pricing_rule else args.get(field, 0))
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, rate=None):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
@@ -404,6 +412,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
if pricing_rule.rate_or_discount == 'Discount Percentage':
item_details.discount_percentage = 0.0
item_details.discount_amount = 0.0
item_details.rate = rate or 0.0
if pricing_rule.rate_or_discount == 'Discount Amount':
item_details.discount_amount = 0.0
@@ -422,6 +431,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None):
item_details.applied_on_items = ','.join(items)
item_details.pricing_rules = ''
item_details.pricing_rule_removed = True
return item_details
@@ -433,9 +443,12 @@ def remove_pricing_rules(item_list):
out = []
for item in item_list:
item = frappe._dict(item)
if item.get('pricing_rules'):
out.append(remove_pricing_rule_for_item(item.get("pricing_rules"),
item, item.item_code))
if item.get("pricing_rules"):
out.append(
remove_pricing_rule_for_item(
item.get("pricing_rules"), item, item.item_code, item.get("price_list_rate")
)
)
return out

View File

@@ -630,6 +630,67 @@ class TestPricingRule(unittest.TestCase):
for doc in [si, si1]:
doc.delete()
def test_multiple_pricing_rules_with_min_qty(self):
make_pricing_rule(discount_percentage=20, selling=1, priority=1, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 1")
make_pricing_rule(discount_percentage=10, selling=1, priority=2, min_qty=4,
apply_multiple_pricing_rules=1, title="_Test Pricing Rule with Min Qty - 2")
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
item = si.items[0]
item.stock_qty = 1
si.save()
self.assertFalse(item.discount_percentage)
item.qty = 5
item.stock_qty = 5
si.save()
self.assertEqual(item.discount_percentage, 30)
si.delete()
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 1")
frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule with Min Qty - 2")
def test_remove_pricing_rule(self):
item = make_item("Water Flask")
make_item_price("Water Flask", "_Test Price List", 100)
pricing_rule_record = {
"doctype": "Pricing Rule",
"title": "_Test Water Flask Rule",
"apply_on": "Item Code",
"price_or_product_discount": "Price",
"items": [{
"item_code": "Water Flask",
}],
"selling": 1,
"currency": "INR",
"rate_or_discount": "Discount Percentage",
"discount_percentage": 20,
"company": "_Test Company"
}
rule = frappe.get_doc(pricing_rule_record)
rule.insert()
si = create_sales_invoice(do_not_save=True, item_code="Water Flask")
si.selling_price_list = "_Test Price List"
si.save()
self.assertEqual(si.items[0].price_list_rate, 100)
self.assertEqual(si.items[0].discount_percentage, 20)
self.assertEqual(si.items[0].rate, 80)
si.ignore_pricing_rule = 1
si.save()
self.assertEqual(si.items[0].discount_percentage, 0)
self.assertEqual(si.items[0].rate, 100)
si.delete()
rule.delete()
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
test_dependencies = ["Campaign"]
def make_pricing_rule(**args):
@@ -652,7 +713,7 @@ def make_pricing_rule(**args):
"rate": args.rate or 0.0,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
"priority": 1,
"priority": args.priority or 1,
"discount_amount": args.discount_amount or 0.0,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
@@ -678,6 +739,8 @@ def make_pricing_rule(**args):
if args.get(applicable_for):
doc.db_set(applicable_for, args.get(applicable_for))
return doc
def setup_pricing_rule_data():
if not frappe.db.exists('Campaign', '_Test Campaign'):
frappe.get_doc({

View File

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

View File

@@ -73,7 +73,7 @@ def get_report_pdf(doc, consolidated=True):
'to_date': doc.to_date,
'company': doc.company,
'finance_book': doc.finance_book if doc.finance_book else None,
'account': doc.account if doc.account else None,
'account': [doc.account] if doc.account else None,
'party_type': 'Customer',
'party': [entry.customer],
'presentation_currency': presentation_currency,

View File

@@ -176,8 +176,8 @@ class PurchaseInvoice(BuyingController):
if self.supplier and account.account_type != "Payable":
frappe.throw(
_("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To")), title=_("Invalid Account")
_("Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account.")
.format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), title=_("Invalid Account")
)
self.party_account_currency = account.account_currency
@@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Did not use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.credit_to,
@@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController):
"party": self.supplier,
"due_date": self.due_date,
"against": self.against_expense_account,
"credit": grand_total_in_company_currency,
"credit_in_account_currency": grand_total_in_company_currency \
"credit": base_grand_total,
"credit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,
@@ -535,14 +535,22 @@ class PurchaseInvoice(BuyingController):
voucher_wise_stock_value = {}
if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry',
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
stock_ledger_entries = frappe.get_all("Stock Ledger Entry",
fields = ["voucher_detail_no", "stock_value_difference", "warehouse"],
filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}
)
for d in stock_ledger_entries:
voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation')
and flt(d.base_tax_amount_after_discount_amount)]
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company,
'enable_provisional_accounting_for_non_stock_items'))
purchase_receipt_doc_map = {}
for item in self.get("items"):
if flt(item.base_net_amount):
account_currency = get_account_currency(item.expense_account)
@@ -637,19 +645,23 @@ class PurchaseInvoice(BuyingController):
else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
if auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
'account':service_received_but_not_billed_account}, ['name'])
'account':provisional_account}, ['name'])
if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
if not self.is_internal_transfer():
gl_entries.append(self.get_gl_dict({

View File

@@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
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
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice_from_receipt,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes,
make_purchase_receipt,
@@ -1124,8 +1129,6 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
# create a new supplier to test
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
@@ -1198,6 +1201,45 @@ class TestPurchaseInvoice(unittest.TestCase):
payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
item = create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account(account_name="Provision Account",
parent_account="Current Liabilities - _TC", company="_Test Company")
company = frappe.get_doc('Company', '_Test Company')
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
pi.save()
pi.submit()
# Check GLE for Purchase Invoice
expected_gle = [
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
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

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

View File

@@ -45,6 +45,7 @@ from erpnext.setup.doctype.company.company import update_company_current_month_s
from erpnext.stock.doctype.batch.batch import set_batch_nos
from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so
from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos
from erpnext.stock.utils import calculate_mapped_packed_items_return
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -271,6 +272,9 @@ class SalesInvoice(SellingController):
self.process_common_party_accounting()
def validate_pos_return(self):
if self.is_consolidated:
# pos return is already validated in pos invoice
return
if self.is_pos and self.is_return:
total_amount_in_payments = 0
@@ -293,7 +297,7 @@ class SalesInvoice(SellingController):
filters={ invoice_or_credit_note: self.name },
pluck="pos_closing_entry"
)
if pos_closing_entry:
if pos_closing_entry and pos_closing_entry[0]:
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
@@ -586,7 +590,10 @@ class SalesInvoice(SellingController):
frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable":
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
msg = _("Please ensure {} account {} is a Receivable account.").format(
frappe.bold("Debit To"),
frappe.bold(self.debit_to)
) + " "
msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account"))
@@ -745,8 +752,11 @@ class SalesInvoice(SellingController):
def update_packing_list(self):
if cint(self.update_stock) == 1:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
if cint(self.is_return) and self.return_against:
calculate_mapped_packed_items_return(self)
else:
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
make_packing_list(self)
else:
self.set('packed_items', [])
@@ -879,11 +889,11 @@ class SalesInvoice(SellingController):
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total)
else self.base_grand_total, self.precision("base_grand_total"))
if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
gl_entries.append(
self.get_gl_dict({
"account": self.debit_to,
@@ -891,8 +901,8 @@ class SalesInvoice(SellingController):
"party": self.customer,
"due_date": self.due_date,
"against": self.against_income_account,
"debit": grand_total_in_company_currency,
"debit_in_account_currency": grand_total_in_company_currency \
"debit": base_grand_total,
"debit_in_account_currency": base_grand_total \
if self.party_account_currency==self.company_currency else grand_total,
"against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
"against_voucher_type": self.doctype,

View File

@@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp
from erpnext.accounts.utils import PaymentEntryUnlinkError
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data
from erpnext.controllers.accounts_controller import update_invoice_status
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.regional.india.utils import get_ewb_data
@@ -1614,6 +1615,56 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_rounding_adjustment_3(self):
si = create_sales_invoice(do_not_save=True)
si.items = []
for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]:
si.append("items", {
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": d[1],
"rate": d[0],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
})
for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]:
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": tax_account,
"description": tax_account,
"rate": 6,
"cost_center": "_Test Cost Center - _TC",
"included_in_print_rate": 1
})
si.save()
si.submit()
self.assertEqual(si.net_total, 4007.16)
self.assertEqual(si.grand_total, 4488.02)
self.assertEqual(si.total_taxes_and_charges, 480.86)
self.assertEqual(si.rounding_adjustment, -0.02)
expected_values = dict((d[0], d) for d in [
[si.debit_to, 4488.0, 0.0],
["_Test Account Service Tax - _TC", 0.0, 240.43],
["_Test Account VAT - _TC", 0.0, 240.43],
["Sales - _TC", 0.0, 4007.15],
["Round Off - _TC", 0.01, 0]
])
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
order by account asc""", si.name, as_dict=1)
debit_credit_diff = 0
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
debit_credit_diff += (gle.debit - gle.credit)
self.assertEqual(debit_credit_diff, 0)
def test_sales_invoice_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
@@ -1800,47 +1851,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
frappe.set_user("Administrator")
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
self.assertRaises(frappe.ValidationError, pda1.submit)
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
@@ -2217,9 +2227,9 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.48],
["2021-09-30", 5041.1, 26352.58]
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
["2021-09-30", 5041.1, 26407.22]
]
for i, schedule in enumerate(asset.schedules):
@@ -2267,12 +2277,12 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48, True],
["2021-06-30", 20000.0, 21311.48, True],
["2022-06-30", 20000.0, 41311.48, False],
["2023-06-30", 20000.0, 61311.48, False],
["2024-06-30", 20000.0, 81311.48, False],
["2025-06-06", 18688.52, 100000.0, False]
["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61366.12, False],
["2024-06-30", 20000.0, 81366.12, False],
["2025-06-06", 18633.88, 100000.0, False]
]
for i, schedule in enumerate(asset.schedules):
@@ -2370,15 +2380,58 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_update_invoice_status(self):
today = nowdate()
# Sales Invoice without Payment Schedule
si = create_sales_invoice(posting_date=add_days(today, -5))
# Sales Invoice with Payment Schedule
si_with_payment_schedule = create_sales_invoice(do_not_submit=True)
si_with_payment_schedule.extend("payment_schedule", [
{
"due_date": add_days(today, -5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
},
{
"due_date": add_days(today, 5),
"invoice_portion": 50,
"payment_amount": si_with_payment_schedule.grand_total / 2
}
])
si_with_payment_schedule.submit()
for invoice in (si, si_with_payment_schedule):
invoice.db_set("status", "Unpaid")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue")
invoice.db_set("status", "Unpaid and Discounted")
update_invoice_status()
invoice.reload()
self.assertEqual(invoice.status, "Overdue and Discounted")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
si = frappe.copy_doc(test_records[2])
frappe.db.set_value('Item', si.get('items')[0].item_code, 'grant_commission', 1)
frappe.db.set_value('Item', si.get('items')[1].item_code, 'grant_commission', 0)
item = copy.deepcopy(si.get('items')[0])
item.update({
"qty": 1,
"rate": 500,
"grant_commission": 1
})
si.append("items", item)
item = copy.deepcopy(si.get('items')[1])
item.update({
"qty": 1,
"rate": 500,
})
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
@@ -2431,6 +2484,74 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
si.set_posting_time = 1
si.posting_date = '2019-01-01'
si.debit_to = '_Test Receivable USD - _TC'
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-01"
si.items[0].service_end_date = "2019-03-30"
si.items[0].deferred_expense_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
pda1.submit()
expected_gle = [
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
[deferred_account, 2089.89, 0.0, "2019-01-28"],
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
[deferred_account, 1887.64, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
[deferred_account, 2022.47, 0.0, "2019-03-15"]
]
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.credit)
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@@ -832,6 +832,7 @@
},
{
"default": "0",
"fetch_from": "item_code.grant_commission",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
@@ -841,7 +842,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-10-05 12:24:54.968907",
"modified": "2022-02-24 14:41:36.392560",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
@@ -851,3 +852,4 @@
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@@ -46,7 +46,7 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_account_head(tax, doc)
validate_account_head(tax.idx, tax.account_head, doc.company)
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
@@ -55,5 +55,8 @@ def validate_disabled(doc):
frappe.throw(_("Disabled template must not be default template"))
def validate_for_tax_category(doc):
if not doc.tax_category:
return
if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}):
frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))

View File

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

View File

@@ -2,12 +2,13 @@
"actions": [],
"allow_rename": 1,
"autoname": "field:title",
"creation": "2018-11-22 23:38:39.668804",
"creation": "2022-01-19 01:09:28.920486",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"title"
"title",
"disabled"
],
"fields": [
{
@@ -18,14 +19,21 @@
"label": "Title",
"reqd": 1,
"unique": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-03-03 11:50:38.748872",
"modified": "2022-01-18 21:13:41.161017",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -65,5 +73,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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

View File

@@ -221,7 +221,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
debit_credit_diff += flt(d.credit)
round_off_account_exists = True
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)):
gl_map.remove(round_off_gle)
return

View File

@@ -59,7 +59,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
currency = party.default_currency if party.get("default_currency") else get_company_currency(company)
currency = party.get("default_currency") or currency or get_company_currency(company)
party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address)
set_contact_details(party_details, party, party_type)
@@ -308,7 +308,7 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren
.format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency)
def validate_party_accounts(doc):
from erpnext.controllers.accounts_controller import validate_account_head
companies = []
for account in doc.get("accounts"):
@@ -331,6 +331,9 @@ def validate_party_accounts(doc):
if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency:
frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency"))
# validate if account is mapped for same company
validate_account_head(account.idx, account.account, account.company)
@frappe.whitelist()
def get_due_date(posting_date, party_type, party, company=None, bill_date=None):

View File

@@ -1,23 +0,0 @@
{
"align_labels_right": 0,
"creation": "2017-08-08 12:33:04.773099",
"custom_format": 1,
"disabled": 0,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tfont-family: Tahoma, sans-serif;\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n<p class=\"text-center\">\n\t{{ doc.company }}<br>\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t<b>{{ _(\"GSTIN\") }}:</b>{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"<br>GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t<br>\n\t{% if doc.docstatus == 0 %}\n\t\t<b>{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}</b><br>\n\t{% else %}\n\t\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t{% endif %}\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"<br>\", \" \") %}\n\t\t<b>{{ _(\"Customer\") }}:</b><br>\n\t\t{{ doc.customer_name }}<br>\n\t\t{{ customer_address }}\n\t{% endif %}\n</p>\n\n<hr>\n<table class=\"table table-condensed cart no-border\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"50%\">{{ _(\"Item\") }}</b></th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t<br><b>{{ _(\"HSN/SAC\") }}:</b> {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"Serial No\") }}:</b> {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}<br>@ {{ item.rate }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t{% if doc.flags.show_inclusive_tax_in_print %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% else %}\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t</td>\n\t\t\t{% endif %}\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t {%- if (not row.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) and row.tax_amount != 0 -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 70%\">\n\t\t\t\t\t{{ row.description }}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t {%- endif -%}\n\t\t{%- endfor -%}\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- if doc.change_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 75%\">\n\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t{%- endif -%}\n\t</tbody>\n</table>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"modified": "2020-04-29 16:39:12.936215",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST POS Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

View File

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

View File

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

View File

@@ -120,11 +120,11 @@ def check_opening_balance(asset, liability, equity):
opening_balance = 0
float_precision = cint(frappe.db.get_default("float_precision")) or 2
if asset:
opening_balance = flt(asset[0].get("opening_balance", 0), float_precision)
opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision)
if liability:
opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision)
opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision)
if equity:
opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision)
opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision)
opening_balance = flt(opening_balance, float_precision)
if opening_balance:

View File

@@ -354,9 +354,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account:
account = d.parent_account_name
# if not accounts_by_name.get(account):
# continue
for company in companies:
accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
@@ -367,7 +364,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
accounts = get_accounts(root_type, companies)
if not accounts:
return None, None, None
@@ -396,7 +393,7 @@ def update_parent_account_names(accounts):
for account in accounts:
if account.parent_account:
account["parent_account_name"] = name_to_account_map[account.parent_account]
account["parent_account_name"] = name_to_account_map.get(account.parent_account)
return accounts
@@ -419,12 +416,19 @@ def get_subsidiary_companies(company):
return frappe.db.sql_list("""select name from `tabCompany`
where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt))
def get_accounts(root_type, filters):
return frappe.db.sql(""" select name, is_group, company,
parent_account, lft, rgt, root_type, report_type, account_name, account_number
from
`tabAccount` where company = %s and root_type = %s
""" , (filters.get('company'), root_type), as_dict=1)
def get_accounts(root_type, companies):
accounts = []
added_accounts = []
for company in companies:
for account in frappe.get_all("Account", fields=["name", "is_group", "company",
"parent_account", "lft", "rgt", "root_type", "report_type", "account_name", "account_number"],
filters={"company": company, "root_type": root_type}):
if account.account_name not in added_accounts:
accounts.append(account)
added_accounts.append(account.account_name)
return accounts
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = []

View File

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

View File

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

View File

@@ -285,7 +285,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
total_row = {
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
"currency": company_currency
"currency": company_currency,
"opening_balance": 0.0
}
for row in out:
@@ -297,6 +298,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency
total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"])
total_row["opening_balance"] += row["opening_balance"]
row["total"] = ""
if "total" in total_row:

View File

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

View File

@@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif group_by_voucher_consolidated:
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
if filters.get("include_dimensions"):
for dim in accounting_dimensions:
keylist.append(gle.get(dim))
keylist.append(gle.get("cost_center"))
key = tuple(keylist)
if key not in consolidated_gle:
consolidated_gle.setdefault(key, gle)
@@ -595,14 +597,14 @@ def get_columns(filters):
"fieldname": dim.fieldname,
"width": 100
})
columns.extend([
{
columns.append({
"label": _("Cost Center"),
"options": "Cost Center",
"fieldname": "cost_center",
"width": 100
},
})
columns.extend([
{
"label": _("Against Voucher Type"),
"fieldname": "against_voucher_type",

View File

@@ -42,6 +42,11 @@ frappe.query_reports["Gross Profit"] = {
"parent_field": "parent_invoice",
"initial_depth": 3,
"formatter": function(value, row, column, data, default_formatter) {
if (column.fieldname == "sales_invoice" && column.options == "Item" && data.indent == 0) {
column._options = "Sales Invoice";
} else {
column._options = "Item";
}
value = default_formatter(value, row, column, data);
if (data && (data.indent == 0.0 || row[1].content == "Total")) {

View File

@@ -23,7 +23,7 @@ def validate_filters(filters):
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(filters, tds_docs)
gle_map = get_gle_map(tds_docs)
out = []
for name, details in gle_map.items():
@@ -43,7 +43,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit)
total_amount_credited += entry.credit
if tds_deducted:
row = {
@@ -78,7 +78,7 @@ def get_supplier_pan_map():
return supplier_map
def get_gle_map(filters, documents):
def get_gle_map(documents):
# create gle_map of the form
# {"purchase_invoice": list of dict of all gle created for this invoice}
gle_map = {}
@@ -86,7 +86,7 @@ def get_gle_map(filters, documents):
gle = frappe.db.get_all('GL Entry',
{
"voucher_no": ["in", documents],
"credit": (">", 0)
"is_cancelled": 0
},
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
)
@@ -184,21 +184,28 @@ def get_tds_docs(filters):
payment_entries = []
journal_entries = []
tax_category_map = {}
or_filters = {}
bank_accounts = frappe.get_all('Account', {'is_group': 0, 'account_type': 'Bank'}, pluck="name")
tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
pluck="account")
query_filters = {
"credit": ('>', 0),
"account": ("in", tds_accounts),
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
"is_cancelled": 0
"is_cancelled": 0,
"against": ("not in", bank_accounts)
}
if filters.get('supplier'):
query_filters.update({'against': filters.get('supplier')})
if filters.get("supplier"):
del query_filters["account"]
del query_filters["against"]
or_filters = {
"against": filters.get('supplier'),
"party": filters.get('supplier')
}
tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"])
tds_docs = frappe.get_all("GL Entry", filters=query_filters, or_filters=or_filters, fields=["voucher_no", "voucher_type", "against", "party"])
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":

View File

@@ -143,6 +143,7 @@
},
{
"allow_on_submit": 1,
"fetch_from": "item_code.image",
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
@@ -516,7 +517,7 @@
"link_fieldname": "asset"
}
],
"modified": "2022-01-19 01:36:51.361485",
"modified": "2022-01-30 20:19:24.680027",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -560,4 +561,4 @@
"states": [],
"title_field": "asset_name",
"track_changes": 1
}
}

View File

@@ -37,6 +37,7 @@ class Asset(AccountsController):
self.validate_asset_values()
self.validate_asset_and_reference()
self.validate_item()
self.validate_cost_center()
self.set_missing_values()
if not self.split_from:
self.prepare_depreciation_data()
@@ -97,6 +98,19 @@ class Asset(AccountsController):
elif item.is_stock_item:
frappe.throw(_("Item {0} must be a non-stock item").format(self.item_code))
def validate_cost_center(self):
if not self.cost_center: return
cost_center_company = frappe.db.get_value('Cost Center', self.cost_center, 'company')
if cost_center_company != self.company:
frappe.throw(
_("Selected Cost Center {} doesn't belongs to {}").format(
frappe.bold(self.cost_center),
frappe.bold(self.company)
),
title=_("Invalid Cost Center")
)
def validate_in_use_date(self):
if not self.available_for_use_date:
frappe.throw(_("Available for use date is required"))
@@ -328,7 +342,8 @@ class Asset(AccountsController):
return value_after_depreciation
# used when depreciation schedule needs to be modified due to increase in asset life
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self):
start = []
num_of_depreciations_completed = 0
@@ -403,11 +418,12 @@ class Asset(AccountsController):
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
.format(row.idx))
.format(row.idx), title=_("Invalid Schedule"))
if not row.depreciation_start_date:
if not self.available_for_use_date:
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
frappe.throw(_("Row {0}: Depreciation Start Date is required")
.format(row.idx), title=_("Invalid Schedule"))
row.depreciation_start_date = get_last_day(self.available_for_use_date)
if not self.is_existing_asset:
@@ -425,8 +441,9 @@ class Asset(AccountsController):
else:
self.number_of_depreciations_booked = 0
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
if flt(row.total_number_of_depreciations) <= cint(self.number_of_depreciations_booked):
frappe.throw(_("Row {0}: Total Number of Depreciations cannot be less than or equal to Number of Depreciations Booked")
.format(row.idx), title=_("Invalid Schedule"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
@@ -611,7 +628,17 @@ class Asset(AccountsController):
return purchase_document
def get_fixed_asset_account(self):
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
if not fixed_asset_account:
frappe.throw(
_("Set {0} in asset category {1} for company {2}").format(
frappe.bold("Fixed Asset Account"),
frappe.bold(self.asset_category),
frappe.bold(self.company),
),
title=_("Account not Found"),
)
return fixed_asset_account
def get_cwip_account(self, cwip_enabled=False):
cwip_account = None

View File

@@ -207,9 +207,9 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
("Debtors - _TC", 25000.0, 0.0)
)
@@ -542,10 +542,10 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2030-12-31", 27534.25, 27534.25],
["2031-12-31", 30000.0, 57534.25],
["2032-12-31", 30000.0, 87534.25],
["2033-01-30", 2465.75, 90000.0]
['2030-12-31', 27616.44, 27616.44],
['2031-12-31', 30000.0, 57616.44],
['2032-12-31', 30000.0, 87616.44],
['2033-01-30', 2383.56, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -595,10 +595,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 28493.15, 28493.15],
["2031-12-31", 35753.43, 64246.58],
["2032-12-31", 17876.71, 82123.29],
["2033-06-06", 5376.71, 87500.0]
['2030-12-31', 28630.14, 28630.14],
['2031-12-31', 35684.93, 64315.07],
['2032-12-31', 17842.47, 82157.54],
['2033-06-06', 5342.46, 87500.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -631,10 +631,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 11780.82, 11780.82],
["2031-12-31", 44109.59, 55890.41],
["2032-12-31", 22054.8, 77945.21],
["2033-07-12", 9554.79, 87500.0]
["2030-12-31", 11849.32, 11849.32],
["2031-12-31", 44075.34, 55924.66],
["2032-12-31", 22037.67, 77962.33],
["2033-07-12", 9537.67, 87500.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -693,7 +693,7 @@ class TestDepreciationBasics(AssetSetup):
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = getdate("2019-12-31"),
available_for_use_date = getdate("2020-01-01"),
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
depreciation_start_date = getdate("2020-07-01"),
@@ -704,7 +704,7 @@ class TestDepreciationBasics(AssetSetup):
["2020-07-01", 15000, 15000],
["2021-07-01", 30000, 45000],
["2022-07-01", 30000, 75000],
["2022-12-31", 15000, 90000]
["2023-01-01", 15000, 90000]
]
for i, schedule in enumerate(asset.schedules):
@@ -871,8 +871,9 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
def test_number_of_depreciations(self):
"""Tests if an error is raised when number_of_depreciations_booked > total_number_of_depreciations."""
"""Tests if an error is raised when number_of_depreciations_booked >= total_number_of_depreciations."""
# number_of_depreciations_booked > total_number_of_depreciations
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
@@ -887,6 +888,21 @@ class TestDepreciationBasics(AssetSetup):
self.assertRaises(frappe.ValidationError, asset.save)
# number_of_depreciations_booked = total_number_of_depreciations
asset_2 = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
total_number_of_depreciations = 5,
expected_value_after_useful_life = 10000,
depreciation_start_date = "2020-07-01",
opening_accumulated_depreciation = 10000,
number_of_depreciations_booked = 5,
do_not_save = 1
)
self.assertRaises(frappe.ValidationError, asset_2.save)
def test_depreciation_start_date_is_before_purchase_date(self):
asset = create_asset(
item_code = "Macbook Pro",
@@ -1027,6 +1043,82 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(len(asset.schedules), 1)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.submit()
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
self.assertEqual(len(asset.schedules), 6)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.save()
self.assertEqual(len(asset.schedules), 9)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
item_code = "Macbook Pro",
@@ -1106,6 +1198,15 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(gle, expected_gle)
self.assertEqual(asset.get("value_after_depreciation"), 0)
def test_asset_cost_center(self):
asset = create_asset(is_existing_asset = 1, do_not_save=1)
asset.cost_center = "Main - WP"
self.assertRaises(frappe.ValidationError, asset.submit)
asset.cost_center = "Main - _TC"
asset.submit()
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category()

View File

@@ -68,6 +68,28 @@ frappe.ui.form.on('Asset Repair', {
});
frappe.ui.form.on('Asset Repair Consumed Item', {
item_code: function(frm, cdt, cdn) {
var item = locals[cdt][cdn];
let item_args = {
'item_code': item.item_code,
'warehouse': frm.doc.warehouse,
'qty': item.consumed_quantity,
'serial_no': item.serial_no,
'company': frm.doc.company
};
frappe.call({
method: 'erpnext.stock.utils.get_incoming_rate',
args: {
args: item_args
},
callback: function(r) {
frappe.model.set_value(cdt, cdn, 'valuation_rate', r.message);
}
});
},
consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);

View File

@@ -13,12 +13,10 @@
],
"fields": [
{
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Valuation Rate",
"read_only": 1
"label": "Valuation Rate"
},
{
"fieldname": "consumed_quantity",
@@ -49,7 +47,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-11-11 18:23:00.492483",
"modified": "2022-02-08 17:37:20.028290",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",

View File

@@ -316,6 +316,16 @@ class PurchaseOrder(BuyingController):
'target_ref_field': 'stock_qty',
'source_field': 'stock_qty'
})
self.status_updater.append({
'source_dt': 'Purchase Order Item',
'target_dt': 'Packed Item',
'target_field': 'ordered_qty',
'target_parent_dt': 'Sales Order',
'target_parent_field': '',
'join_field': 'sales_order_packed_item',
'target_ref_field': 'qty',
'source_field': 'stock_qty'
})
def update_delivered_qty_in_sales_order(self):
"""Update delivered qty in Sales Order for drop ship"""

View File

@@ -3,9 +3,9 @@
import json
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, flt, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -27,7 +27,7 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPurchaseOrder(unittest.TestCase):
class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_receipt(self):
po = create_purchase_order(do_not_submit=True)
self.assertRaises(frappe.ValidationError, make_purchase_receipt, po.name)
@@ -682,17 +682,18 @@ class TestPurchaseOrder(unittest.TestCase):
bin1 = frappe.db.get_value("Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
# Submit PO
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
bin2 = frappe.db.get_value("Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty"], as_dict=1)
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"], as_dict=1)
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
self.assertNotEqual(bin1.modified, bin2.modified)
# Create stock transfer
rm_item = [{"item_code":"_Test FG Item","rm_item_code":"_Test Item","item_name":"_Test Item",

View File

@@ -63,6 +63,7 @@
"material_request_item",
"sales_order",
"sales_order_item",
"sales_order_packed_item",
"supplier_quotation",
"supplier_quotation_item",
"col_break5",
@@ -837,21 +838,30 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
},
{
"fieldname": "sales_order_packed_item",
"fieldtype": "Data",
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-30 20:06:26.712097",
"modified": "2022-02-02 13:10:18.398976",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"search_fields": "item_name",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@@ -1,9 +1,9 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
@@ -16,7 +16,7 @@ from erpnext.stock.doctype.item.test_item import make_item
from erpnext.templates.pages.rfq import check_supplier_has_docname_access
class TestRequestforQuotation(unittest.TestCase):
class TestRequestforQuotation(FrappeTestCase):
def test_quote_status(self):
rfq = make_request_for_quotation()

View File

@@ -1,10 +1,10 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import unittest
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.party import get_due_date
from erpnext.exceptions import PartyDisabled
@@ -13,7 +13,7 @@ test_dependencies = ['Payment Term', 'Payment Terms Template']
test_records = frappe.get_test_records('Supplier')
class TestSupplier(unittest.TestCase):
class TestSupplier(FrappeTestCase):
def test_get_supplier_group_details(self):
doc = frappe.new_doc("Supplier Group")
doc.supplier_group_name = "_Testing Supplier Group"

View File

@@ -3,12 +3,12 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
class TestPurchaseOrder(unittest.TestCase):
class TestPurchaseOrder(FrappeTestCase):
def test_make_purchase_order(self):
from erpnext.buying.doctype.supplier_quotation.supplier_quotation import make_purchase_order

View File

@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
class TestSupplierScorecard(unittest.TestCase):
class TestSupplierScorecard(FrappeTestCase):
def test_create_scorecard(self):
doc = make_supplier_scorecard().insert()
@@ -49,7 +49,7 @@ valid_scorecard = [
"min_grade":0.0,"name":"Very Poor",
"prevent_rfqs":1,
"notify_supplier":0,
"doctype":"Supplier Scorecard Standing",
"doctype":"Supplier Scorecard Scoring Standing",
"max_grade":30.0,
"prevent_pos":1,
"warn_pos":0,
@@ -65,7 +65,7 @@ valid_scorecard = [
"name":"Poor",
"prevent_rfqs":1,
"notify_supplier":0,
"doctype":"Supplier Scorecard Standing",
"doctype":"Supplier Scorecard Scoring Standing",
"max_grade":50.0,
"prevent_pos":0,
"warn_pos":0,
@@ -81,7 +81,7 @@ valid_scorecard = [
"name":"Average",
"prevent_rfqs":0,
"notify_supplier":0,
"doctype":"Supplier Scorecard Standing",
"doctype":"Supplier Scorecard Scoring Standing",
"max_grade":80.0,
"prevent_pos":0,
"warn_pos":0,
@@ -97,7 +97,7 @@ valid_scorecard = [
"name":"Excellent",
"prevent_rfqs":0,
"notify_supplier":0,
"doctype":"Supplier Scorecard Standing",
"doctype":"Supplier Scorecard Scoring Standing",
"max_grade":100.0,
"prevent_pos":0,
"warn_pos":0,

View File

@@ -1,12 +1,12 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
class TestSupplierScorecardCriteria(unittest.TestCase):
class TestSupplierScorecardCriteria(FrappeTestCase):
def test_variables_exist(self):
delete_test_scorecards()
for d in test_good_criteria:

View File

@@ -1,16 +1,16 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.supplier_scorecard_variable.supplier_scorecard_variable import (
VariablePathNotFound,
)
class TestSupplierScorecardVariable(unittest.TestCase):
class TestSupplierScorecardVariable(FrappeTestCase):
def test_variable_exist(self):
for d in test_existing_variables:
my_doc = frappe.get_doc("Supplier Scorecard Variable", d.get("name"))

View File

@@ -2,10 +2,10 @@
# For license information, please see license.txt
import unittest
from datetime import datetime
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.report.procurement_tracker.procurement_tracker import execute
@@ -14,7 +14,7 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
class TestProcurementTracker(unittest.TestCase):
class TestProcurementTracker(FrappeTestCase):
def test_result_for_procurement_tracker(self):
filters = {
'company': '_Test Procurement Company',

View File

@@ -3,9 +3,9 @@
# Compiled at: 2019-05-06 09:51:46
# Decompiled by https://python-decompiler.com
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -15,7 +15,7 @@ from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestSubcontractedItemToBeReceived(unittest.TestCase):
class TestSubcontractedItemToBeReceived(FrappeTestCase):
def test_pending_and_received_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')

View File

@@ -4,9 +4,9 @@
# Decompiled by https://python-decompiler.com
import json
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -16,7 +16,7 @@ from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcont
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
def test_pending_and_transferred_qty(self):
po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")

View File

@@ -0,0 +1,52 @@
## Version 13.19.0 Release Notes
### Features & Enhancements
- Allow user to change the parent company ([#28983](https://github.com/frappe/erpnext/pull/28983))
- Option to exclude holidays while marking monthly attendance ([#29185](https://github.com/frappe/erpnext/pull/29185))
- Early payment discount on sales & purchase orders ([#29101](https://github.com/frappe/erpnext/pull/29101))
### Fixes
- Filter query in bank reconciliation tool ([#29098](https://github.com/frappe/erpnext/pull/29098))
- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324))
- GL Entries for loan repayment via Salary ([#29169](https://github.com/frappe/erpnext/pull/29169))
- Group by Cost Center in General Ledger report only if include_dimensions is checked ([#28883](https://github.com/frappe/erpnext/pull/28883))
- Filter for leave period in Bulk Leave Policy Assignment ([#29272](https://github.com/frappe/erpnext/pull/29272))
- Update idx after updating items in so/po ([#29134](https://github.com/frappe/erpnext/pull/29134))
- Avoid resetting default warehouse fields for Manufacture Entry ([#29257](https://github.com/frappe/erpnext/pull/29257))
- Don't validate FG in repack entry ([#29271](https://github.com/frappe/erpnext/pull/29271))
- Map Accounting Dimensions for Bank Entry against Payroll Entry ([#29142](https://github.com/frappe/erpnext/pull/29142))
- Ignore cancelled SLEs ([#29303](https://github.com/frappe/erpnext/pull/29303))
- Show work order progress bar even it is closed ([#29312](https://github.com/frappe/erpnext/pull/29312))
- Incorrect serial no valuation report showing cancelled entries ([#29172](https://github.com/frappe/erpnext/pull/29172))
- Not able to make a reverse journal entry ([#29125](https://github.com/frappe/erpnext/pull/29125))
- Show ledger balance in Accounts Receivable and Payable summary ([#29135](https://github.com/frappe/erpnext/pull/29135))
- Add stock queue in SLE for FIFO valuation method ([#29302](https://github.com/frappe/erpnext/pull/29302))
- Threshold fields shows incorrect currency ([#29270](https://github.com/frappe/erpnext/pull/29270))
- Added patch to trim whitespace from the serial numbers ([#29306](https://github.com/frappe/erpnext/pull/29306))
- Task Depends on not removed from Gantt chart ([#28309](https://github.com/frappe/erpnext/pull/28309))
- Earned Leave allocation from Leave Policy Assignment ([#29163](https://github.com/frappe/erpnext/pull/29163))
- Exclude existing serial numbers while auto creating new serial numbers ([#29292](https://github.com/frappe/erpnext/pull/29292))
- Deferred revenue booking for multi currency invoices via Journal Entry ([#29115](https://github.com/frappe/erpnext/pull/29115))
- Fixed autoname generated for Job Applicant ([#29260](https://github.com/frappe/erpnext/pull/29260))
- Incorrect scrap item quantity calculated in the Manufacture type stock entry ([#29179](https://github.com/frappe/erpnext/pull/29179))
- Inconsistency in calculating outstanding amount ([#29176](https://github.com/frappe/erpnext/pull/29176))
- Accounts are coming from different company in the dropdown ([#29280](https://github.com/frappe/erpnext/pull/29280))
- Can't create debit note with zero quantity ([#28994](https://github.com/frappe/erpnext/pull/28994))
- "Update Cost" should ignore overridden routing times ([#29154](https://github.com/frappe/erpnext/pull/29154))
- Modifying Opening invoice creation tool timestamp ([#29127](https://github.com/frappe/erpnext/pull/29127))
- Future recurring period calculation ([#29083](https://github.com/frappe/erpnext/pull/29083))
- India localization: NIL Rated, Exempted and Non GST Invoices in GSTR-1 report ([#29208](https://github.com/frappe/erpnext/pull/29208))
- Purchase to Stock UOM conversion on Production Plan ([#28570](https://github.com/frappe/erpnext/pull/28570))
- Validation in POS for item batch no stock quantity ([#28907](https://github.com/frappe/erpnext/pull/28907))
- Shopping cart total quantity ([#29076](https://github.com/frappe/erpnext/pull/29076))
- POS items added to cart despite low quantity ([#29126](https://github.com/frappe/erpnext/pull/29126))
- Exclude unpublished items while fetching items from other item groups ([#29211](https://github.com/frappe/erpnext/pull/29211))
- Get project from PO into payment entry ([#29182](https://github.com/frappe/erpnext/pull/29182))
- Cover case when all material needs to be bought ([#29326](https://github.com/frappe/erpnext/pull/29326))
- Validate setup on clicking Mark Attendance button in Shift Type ([#29146](https://github.com/frappe/erpnext/pull/29146))
- Can't ignore pricing rule for one particular POS invoice ([#29222](https://github.com/frappe/erpnext/pull/29222))
- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383))
- Not able to submit salary slips from amended payroll entry. ([#29228](https://github.com/frappe/erpnext/pull/29228))
- Tax and Charges template not getting fetched based on tax category assigned ([#29092](https://github.com/frappe/erpnext/pull/29092))

View File

@@ -0,0 +1,32 @@
## Version 13.20.0 Release Notes
### Features & Enhancements
- Provisional accounting for expenses ([#29451](https://github.com/frappe/erpnext/pull/29451))
### Fixes
- Incorrect number of items fetched while creating delivery note ([#29454](https://github.com/frappe/erpnext/pull/29454))
- Incorrect raw materials quantity in manufacture stock entry ([#29419](https://github.com/frappe/erpnext/pull/29419))
- Refactored the update_serial_no function for old Maintenance Visits ([#28843](https://github.com/frappe/erpnext/pull/28843))
- Ignore empty customer/supplier in item query ([#29610](https://github.com/frappe/erpnext/pull/29610))
- The "Bypass Credit Limit Check" from customer has not fetched in the Customer Credit Balance report ([#29367](https://github.com/frappe/erpnext/pull/29367))
- Reset conversion facture after changing the Stock UOM ([#29062](https://github.com/frappe/erpnext/pull/29062))
- Cart Items rendering issue ([#29398](https://github.com/frappe/erpnext/pull/29398))
- Honour 'include holidays' setting while marking attendance for leave application ([#29425](https://github.com/frappe/erpnext/pull/29425))
- Cost of poor quality report time filters not working ([#28958](https://github.com/frappe/erpnext/pull/28958))
- Incorrect packing items getting fetched on Sales Return / Credit Note ([#28607](https://github.com/frappe/erpnext/pull/28607))
- Regenerate packing items on newly mapped doc ([#29642](https://github.com/frappe/erpnext/pull/29642))
- From Time and To Time not updated in drag and drop action for Course Schedule ([#29114](https://github.com/frappe/erpnext/pull/29114))
- Employee: set user image and validate user id only if user data is found ([#29452](https://github.com/frappe/erpnext/pull/29452))
- Clear Depreciation Schedule before modification ([#28507](https://github.com/frappe/erpnext/pull/28507))
- Fixed shopping cart qty badge ([#29077](https://github.com/frappe/erpnext/pull/29077))
- Fetch "transfer material against" from BOM ([#29435](https://github.com/frappe/erpnext/pull/29435))
- Cart & Popup Logic of Item variant without Website Item ([#29383](https://github.com/frappe/erpnext/pull/29383))
- Timesheets: calculate to time based on from time and hours ([#28589](https://github.com/frappe/erpnext/pull/28589))
- Dynamically compute BOM Level ([#29522](https://github.com/frappe/erpnext/pull/29522))
- Contact duplication on converting lead to customer ([#29337](https://github.com/frappe/erpnext/pull/29337))
- Fixed populate practitioner selected in form to check availability popup ([#29405](https://github.com/frappe/erpnext/pull/29405))
- Compute batch ledger in python ([#29324](https://github.com/frappe/erpnext/pull/29324))
- Incorrect packing list for recurring items & code cleanup ([#29456](https://github.com/frappe/erpnext/pull/29456))
- Opening invoice creation tool can fetch multiple accounting dimension ([#29407](https://github.com/frappe/erpnext/pull/29407))

View File

@@ -0,0 +1,49 @@
## Version 13.21.0 Release Notes
### Features & Enhancements
- Provisional accounting for expenses ([#29451](https://github.com/frappe/erpnext/pull/29451))
- Allowing non stock items in POS ([#29556](https://github.com/frappe/erpnext/pull/29556))
- Option to disable Item Tax Template and Tax Category ([#29349](https://github.com/frappe/erpnext/pull/29349))
### Fixes
- Ignore linked invoices on Journal Entry cancel ([#29641](https://github.com/frappe/erpnext/pull/29641))
- Do not hide Loan Repayment Entry field in salary slip ([#29535](https://github.com/frappe/erpnext/pull/29535))
- Coupon code is applied even if ignore_pricing_rule is enabled ([#29859](https://github.com/frappe/erpnext/pull/29859))
- Reserved for Production calculation considered closed work orders ([#29723](https://github.com/frappe/erpnext/pull/29723))
- Disable rounded total in opening invoice creation tool ([#29789](https://github.com/frappe/erpnext/pull/29789))
- Report GSTR-1 minor fixes ([#29700](https://github.com/frappe/erpnext/pull/29700))
- Ignore rate validation for work order ([#29690](https://github.com/frappe/erpnext/pull/29690))
- Incorrect provisional profit and loss in balance sheet ([#29601](https://github.com/frappe/erpnext/pull/29601))
- Multiple WO for a single Production Plan Item ([#29603](https://github.com/frappe/erpnext/pull/29603))
- Validation for invalid serial nos at POS invoice level ([#29447](https://github.com/frappe/erpnext/pull/29447))
- Incorrect Grand Total in case of inclusive taxes on item ([#29701](https://github.com/frappe/erpnext/pull/29701))
- Currency in bank reconciliation chart ([#29709](https://github.com/frappe/erpnext/pull/29709))
- Set Pending Qty in Prod Plan after updating Work Order ([#29705](https://github.com/frappe/erpnext/pull/29705))
- Enable Allow on Submit for 'Is Active' field in Salary Structure ([#29630](https://github.com/frappe/erpnext/pull/29630))
- Bypass "Validate Selling Price for Item Against Purchase Rate or Valuation Rate" for free items ([#29359](https://github.com/frappe/erpnext/pull/29359))
- Generate Warehouse wise FIFO Queue always and later aggregate if required ([#29788](https://github.com/frappe/erpnext/pull/29788))
- Fixes in TDS payable monthly report ([#29791](https://github.com/frappe/erpnext/pull/29791))
- Incorrect pricing rule filtering on selecting first item ([#29778](https://github.com/frappe/erpnext/pull/29778))
- Stock Ageing Transfer Bucket logic for Repack Entry with split batch rows ([#29816](https://github.com/frappe/erpnext/pull/29816))
- Incorrect packing list for recurring items & code cleanup ([#29456](https://github.com/frappe/erpnext/pull/29456))
- Cost center validation of asset ([#29373](https://github.com/frappe/erpnext/pull/29373))
- Coupon code item pricing dynamic updation issue in pos screen ([#29599](https://github.com/frappe/erpnext/pull/29599))
- Billed amount in delivery note items ([#29290](https://github.com/frappe/erpnext/pull/29290))
- Regenerate packed items on newly mapped doc ([#29642](https://github.com/frappe/erpnext/pull/29642))
- Cannot jump to sales invoice in gross profit report ([#29748](https://github.com/frappe/erpnext/pull/29748))
- Fetch image form item ([#29523](https://github.com/frappe/erpnext/pull/29523))
- Add missing key in Loan ([#29660](https://github.com/frappe/erpnext/pull/29660))
- Weed out disabled variants via sql query instead of pythonic looping separately ([#29639](https://github.com/frappe/erpnext/pull/29639 ())
- Loan repayment via Salary Slip ([#29716](https://github.com/frappe/erpnext/pull/29716))
- Earned leaves not allocated if assignment is created on month-end based on Leave Policy ([#29650](https://github.com/frappe/erpnext/pull/29650))
- Time out error while making work orders from production plan ([#29736](https://github.com/frappe/erpnext/pull/29736))
- Removal of coupon code ([#29896](https://github.com/frappe/erpnext/pull/29896))
- Earned Leave allocation based on joining date fixes ([#29711](https://github.com/frappe/erpnext/pull/29711))
- Total Credit amount in TDS Payable monthly report ([#29907](https://github.com/frappe/erpnext/pull/29907))
- Pricing rule on transactions doesn't work ([#29597](https://github.com/frappe/erpnext/pull/29597))
- Billing status for zero amount reference doc ([#29659](https://github.com/frappe/erpnext/pull/29659))
- Zero rated exports in GSTR-3B report ([#29609](https://github.com/frappe/erpnext/pull/29609))
- Update SO via Work Order made from MR ([#29803](https://github.com/frappe/erpnext/pull/29803))
- Currency in bank reconciliation tool ([#29848](https://github.com/frappe/erpnext/pull/29848))

View File

@@ -0,0 +1,42 @@
## Version 13.22.0 Release Notes
### Features & Enhancements
- feat: Payment Terms Status report (backport #29137) ([#29137](https://github.com/frappe/erpnext/pull/29137))
### Fixes
- fix(LMS): program enrollment does not give any feedback (backport #29922) ([#29922](https://github.com/frappe/erpnext/pull/29922))
- fix: Update SO via Work Order made from MR (attached to SO) (backport #29803) ([#29803](https://github.com/frappe/erpnext/pull/29803))
- fix: org chart connectors not rendered when Employee Naming is set to Full Name ([#29997](https://github.com/frappe/erpnext/pull/29997))
- perf: Weed out disabled variants via sql query instead of pythonic looping separately (backport #29639) ([#29639](https://github.com/frappe/erpnext/pull/29639))
- fix: task status loop ([#26006](https://github.com/frappe/erpnext/pull/26006))
- fix: Commission not applied while making Sales Order from Quotation (backport #29978) ([#29978](https://github.com/frappe/erpnext/pull/29978))
- fix: Validate party account with company (backport #29879) ([#29879](https://github.com/frappe/erpnext/pull/29879))
- fix: add supported currencies for GoCardless (backport #29805) ([#29805](https://github.com/frappe/erpnext/pull/29805))
- fix(asset): no. of depr booked cannot be equal to total no. of depr (backport #29900) ([#29900](https://github.com/frappe/erpnext/pull/29900))
- fix: Fetch conversion factor even if it already existed in row, on item change ([#29917](https://github.com/frappe/erpnext/pull/29917))
- fix(ux): make "allow zero valuation rate" readonly if "s_warehouse" is set ([#29681](https://github.com/frappe/erpnext/pull/29681))
- fix: Block merging items if both have product bundles (backport #29913) ([#29913](https://github.com/frappe/erpnext/pull/29913))
- fix: JobCard TimeLog to_date (backport #29872) ([#29872](https://github.com/frappe/erpnext/pull/29872))
- fix: Stock Ageing Transfer Bucket logic for Repack Entry with split batch rows (backport #29816) ([#29816](https://github.com/frappe/erpnext/pull/29816))
- fix(Salary Slip): TypeError while clearing any amount field in components (backport #29931) ([#29931](https://github.com/frappe/erpnext/pull/29931))
- fix: allow renaming and merging ([#29830](https://github.com/frappe/erpnext/pull/29830))
- fix(pos): minor fixes (backport #29991) ([#29991](https://github.com/frappe/erpnext/pull/29991))
- fix(e-commerce): Unique Shopping Cart Per Logged In User ([#29994](https://github.com/frappe/erpnext/pull/29994))
- fix: currency in bank reconciliation tool (backport #29848) ([#29848](https://github.com/frappe/erpnext/pull/29848))
- fix: Account filter in PSOA (backport #29928) ([#29928](https://github.com/frappe/erpnext/pull/29928))
- fix: Taxjar minor fixes (backport #29942) ([#29942](https://github.com/frappe/erpnext/pull/29942))
- fix: Total taxes and charges in payment entry for multi-currency payments (backport #29977) ([#29977](https://github.com/frappe/erpnext/pull/29977))
- fix(e-invoicing): remove batch no from e-invoices (backport #30084) ([#30084](https://github.com/frappe/erpnext/pull/30084))
- fix: Total Credit amount in TDS Payable monthly report (backport #29907) ([#29907](https://github.com/frappe/erpnext/pull/29907))
- fix: GSTIN filter for GSTR-1 report (backport #29869) ([#29869](https://github.com/frappe/erpnext/pull/29869))
- fix: coupon code is applied even if ignore_pricing_rule is enabled (backport #29859) ([#29859](https://github.com/frappe/erpnext/pull/29859))
- feat: update ordered qty for packed items (backport #29939) ([#29939](https://github.com/frappe/erpnext/pull/29939))
- fix: validate Work Order qty against Production Plan ([#29721](https://github.com/frappe/erpnext/pull/29721))
- fix: Fetch valuation rate for stock items consumed during asset repair ([#29714](https://github.com/frappe/erpnext/pull/29714))
- fix: Email translations (backport #29956) ([#29956](https://github.com/frappe/erpnext/pull/29956))
- fix(Timesheet): fetch exchange rate only if currency is set (backport #30057) ([#30057](https://github.com/frappe/erpnext/pull/30057))
- refactor: removed validation to check zero qty (backport #30015) ([#30015](https://github.com/frappe/erpnext/pull/30015))
- fix(pos): removal of coupon code (backport #29896) ([#29896](https://github.com/frappe/erpnext/pull/29896))
- fix: Error in consolidated financial statements (backport #29771) ([#29771](https://github.com/frappe/erpnext/pull/29771))

View File

@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _, throw
from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied
from frappe.query_builder.functions import Sum
from frappe.utils import (
add_days,
add_months,
@@ -113,7 +114,7 @@ class AccountsController(TransactionBase):
_('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1)
def validate(self):
if not self.get('is_return'):
if not self.get('is_return') and not self.get('is_debit_note'):
self.validate_qty_is_not_zero()
if self.get("_action") and self._action != "update_after_submit":
@@ -190,8 +191,6 @@ class AccountsController(TransactionBase):
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_start_date):
frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx))
def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates()
@@ -409,6 +408,22 @@ class AccountsController(TransactionBase):
if item_qty != len(get_serial_nos(item.get('serial_no'))):
item.set(fieldname, value)
elif (
ret.get("pricing_rule_removed")
and value is not None
and fieldname
in [
"discount_percentage",
"discount_amount",
"rate",
"margin_rate_or_amount",
"margin_type",
"remove_free_item",
]
):
# reset pricing rule fields if pricing_rule_removed
item.set(fieldname, value)
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
@@ -1320,6 +1335,9 @@ class AccountsController(TransactionBase):
payment_schedule['discount_type'] = schedule.discount_type
payment_schedule['discount'] = schedule.discount
if not schedule.invoice_portion:
payment_schedule['payment_amount'] = schedule.payment_amount
self.append("payment_schedule", payment_schedule)
def set_due_date(self):
@@ -1549,13 +1567,12 @@ def validate_taxes_and_charges(tax):
tax.rate = None
def validate_account_head(tax, doc):
company = frappe.get_cached_value('Account',
tax.account_head, 'company')
def validate_account_head(idx, account, company):
account_company = frappe.get_cached_value('Account', account, 'company')
if company != doc.company:
if account_company != company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
.format(idx, frappe.bold(account), frappe.bold(company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc):
@@ -1692,58 +1709,69 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
def update_invoice_status():
"""Updates status as Overdue for applicable invoices. Runs daily."""
today = getdate()
payment_schedule = frappe.qb.DocType("Payment Schedule")
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
WHERE invoice.docstatus = 1
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
AND invoice.outstanding_amount > 0
AND (
{or_condition}
(
(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.grand_total
ELSE invoice.rounded_total
END
)
ELSE (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.base_grand_total
ELSE invoice.base_rounded_total
END
)
END
) - invoice.outstanding_amount
) < (
SELECT SUM(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN ps.payment_amount
ELSE ps.base_payment_amount
END
)
FROM `tabPayment Schedule` ps
WHERE ps.parent = invoice.name
AND ps.due_date < %(today)s
)
)
""".format(
doctype=doctype,
or_condition=(
"invoice.is_pos AND invoice.due_date < %(today)s OR"
if doctype == "Sales Invoice"
else ""
)
), {"today": today}
invoice = frappe.qb.DocType(doctype)
consider_base_amount = invoice.party_account_currency != invoice.currency
payment_amount = (
frappe.qb.terms.Case()
.when(consider_base_amount, payment_schedule.base_payment_amount)
.else_(payment_schedule.payment_amount)
)
payable_amount = (
frappe.qb.from_(payment_schedule)
.select(Sum(payment_amount))
.where(
(payment_schedule.parent == invoice.name)
& (payment_schedule.due_date < today)
)
)
total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.grand_total)
.else_(invoice.rounded_total)
)
base_total = (
frappe.qb.terms.Case()
.when(invoice.disable_rounded_total, invoice.base_grand_total)
.else_(invoice.base_rounded_total)
)
total_amount = (
frappe.qb.terms.Case()
.when(consider_base_amount, base_total)
.else_(total)
)
is_overdue = total_amount - invoice.outstanding_amount < payable_amount
conditions = (
(invoice.docstatus == 1)
& (invoice.outstanding_amount > 0)
& (
invoice.status.like("Unpaid%")
| invoice.status.like("Partly Paid%")
)
& (
((invoice.is_pos & invoice.due_date < today) | is_overdue)
if doctype == "Sales Invoice"
else is_overdue
)
)
status = (
frappe.qb.terms.Case()
.when(invoice.status.like("%Discounted"), "Overdue and Discounted")
.else_("Overdue")
)
frappe.qb.update(invoice).set("status", status).where(conditions).run()
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
if not terms_template:
@@ -1927,7 +1955,8 @@ def update_bin_on_delete(row, doctype):
qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
update_bin_qty(row.item_code, row.warehouse, qty_dict)
if row.warehouse:
update_bin_qty(row.item_code, row.warehouse, qty_dict)
def validate_and_delete_children(parent, data):
deleted_children = []
@@ -2113,6 +2142,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_status_updater()
else:
parent.check_credit_limit()
# reset index of child table
for idx, row in enumerate(parent.get(child_docname), start=1):
row.idx = idx
parent.save()
if parent_doctype == 'Purchase Order':

View File

@@ -70,9 +70,18 @@ class BuyingController(StockController, Subcontracting):
# set contact and address details for supplier, if they are not mentioned
if getattr(self, "supplier", None):
self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'),
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template')))
self.update_if_missing(
get_party_details(
self.supplier,
party_type="Supplier",
doctype=self.doctype,
company=self.company,
party_address=self.get("supplier_address"),
shipping_address=self.get('shipping_address'),
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template'),
ignore_permissions=self.flags.ignore_permissions
)
)
self.set_missing_item_details(for_validate)

View File

@@ -249,6 +249,9 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
del filters['customer']
else:
del filters['supplier']
else:
filters.pop('customer', None)
filters.pop('supplier', None)
description_cond = ''
@@ -737,6 +740,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
item_group = filters.get('item_group')
company = filters.get('company')
taxes = item_doc.taxes or []
while item_group:
@@ -745,7 +749,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_group = item_group_doc.parent_item_group
if not taxes:
return frappe.db.sql(""" SELECT name FROM `tabItem Tax Template` """)
return frappe.get_all('Item Tax Template', filters={'disabled': 0, 'company': company}, as_list=True)
else:
valid_from = filters.get('valid_from')
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
@@ -754,7 +758,7 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
'item_code': filters.get('item_code'),
'posting_date': valid_from,
'tax_category': filters.get('tax_category'),
'company': filters.get('company')
'company': company
}
taxes = _get_item_tax_template(args, taxes, for_validate=True)

View File

@@ -205,7 +205,7 @@ class SellingController(StockController):
valuation_rate_map = {}
for item in self.items:
if not item.item_code:
if not item.item_code or item.is_free_item:
continue
last_purchase_rate, is_stock_item = frappe.get_cached_value(
@@ -252,7 +252,7 @@ class SellingController(StockController):
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
for item in self.items:
if not item.item_code:
if not item.item_code or item.is_free_item:
continue
last_valuation_rate = valuation_rate_map.get(
@@ -386,7 +386,7 @@ class SellingController(StockController):
# Get incoming rate based on original item cost based on valuation method
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
if not d.incoming_rate:
if not (self.get("is_return") and d.incoming_rate):
d.incoming_rate = get_incoming_rate({
"item_code": d.item_code,
"warehouse": d.warehouse,

View File

@@ -400,6 +400,16 @@ class StatusUpdater(Document):
ref_doc = frappe.get_doc(ref_dt, ref_dn)
ref_doc.db_set("per_billed", per_billed)
# set billling status
if hasattr(ref_doc, 'billing_status'):
if ref_doc.per_billed < 0.001:
ref_doc.db_set("billing_status", "Not Billed")
elif ref_doc.per_billed > 99.999999:
ref_doc.db_set("billing_status", "Fully Billed")
else:
ref_doc.db_set("billing_status", "Partly Billed")
ref_doc.set_status(update=True)
def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):

View File

@@ -3,6 +3,7 @@
import json
from collections import defaultdict
from typing import List, Tuple
import frappe
from frappe import _
@@ -17,7 +18,7 @@ from erpnext.accounts.general_ledger import (
from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock import get_warehouse_account_map
from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate
from erpnext.stock.stock_ledger import get_items_to_be_repost
class QualityInspectionRequiredError(frappe.ValidationError): pass
@@ -40,7 +41,10 @@ class StockController(AccountsController):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if cint(erpnext.is_perpetual_inventory_enabled(self.company)):
provisional_accounting_for_non_stock_items = \
cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items:
warehouse_account = get_warehouse_account_map(self.company)
if self.docstatus==1:
@@ -77,17 +81,17 @@ class StockController(AccountsController):
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
def clean_serial_nos(self):
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed
row.serial_no = row.serial_no.replace(",", "\n")
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
# strip preceeding and succeeding spaces for each SN
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
serial_no_list = row.serial_no.split("\n")
serial_no_list = [sn.strip() for sn in serial_no_list]
row.serial_no = "\n".join(serial_no_list)
for row in self.get('packed_items') or []:
if hasattr(row, "serial_no") and row.serial_no:
# remove extra whitespace and store one serial no on each line
row.serial_no = clean_serial_no_string(row.serial_no)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):
@@ -111,17 +115,6 @@ class StockController(AccountsController):
self.check_expense_account(item_row)
# If the item does not have the allow zero valuation rate flag set
# and ( valuation rate not mentioned in an incoming entry
# or incoming entry not found while delivering the item),
# try to pick valuation rate from previous sle or Item master and update in SLE
# Otherwise, throw an exception
if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \
and not item_row.get("allow_zero_valuation_rate"):
sle = self.update_stock_ledger_entries(sle)
# expense account/ target_warehouse / source_warehouse
if item_row.get('target_warehouse'):
warehouse = item_row.get('target_warehouse')
@@ -164,26 +157,6 @@ class StockController(AccountsController):
return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
self.doctype, self.name, currency=self.company_currency, company=self.company)
sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate)
sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate)
if sle.name:
frappe.db.sql("""
update
`tabStock Ledger Entry`
set
stock_value = %(stock_value)s,
valuation_rate = %(valuation_rate)s,
stock_value_difference = %(stock_value_difference)s
where
name = %(name)s""", (sle))
return sle
def get_voucher_details(self, default_expense_account, default_cost_center, sle_map):
if self.doctype == "Stock Reconciliation":
reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose")
@@ -209,33 +182,28 @@ class StockController(AccountsController):
return details
def get_items_and_warehouses(self):
items, warehouses = [], []
def get_items_and_warehouses(self) -> Tuple[List[str], List[str]]:
"""Get list of items and warehouses affected by a transaction"""
if hasattr(self, "items"):
item_doclist = self.get("items")
elif self.doctype == "Stock Reconciliation":
item_doclist = []
data = json.loads(self.reconciliation_json)
for row in data[data.index(self.head_row)+1:]:
d = frappe._dict(zip(["item_code", "warehouse", "qty", "valuation_rate"], row))
item_doclist.append(d)
if not (hasattr(self, "items") or hasattr(self, "packed_items")):
return [], []
if item_doclist:
for d in item_doclist:
if d.item_code and d.item_code not in items:
items.append(d.item_code)
item_rows = (self.get("items") or []) + (self.get("packed_items") or [])
if d.get("warehouse") and d.warehouse not in warehouses:
warehouses.append(d.warehouse)
items = {d.item_code for d in item_rows if d.item_code}
if self.doctype == "Stock Entry":
if d.get("s_warehouse") and d.s_warehouse not in warehouses:
warehouses.append(d.s_warehouse)
if d.get("t_warehouse") and d.t_warehouse not in warehouses:
warehouses.append(d.t_warehouse)
warehouses = set()
for d in item_rows:
if d.get("warehouse"):
warehouses.add(d.warehouse)
return items, warehouses
if self.doctype == "Stock Entry":
if d.get("s_warehouse"):
warehouses.add(d.s_warehouse)
if d.get("t_warehouse"):
warehouses.add(d.t_warehouse)
return list(items), list(warehouses)
def get_stock_ledger_details(self):
stock_ledger = {}
@@ -247,7 +215,7 @@ class StockController(AccountsController):
from
`tabStock Ledger Entry`
where
voucher_type=%s and voucher_no=%s
voucher_type=%s and voucher_no=%s and is_cancelled = 0
""", (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries:
@@ -287,11 +255,7 @@ class StockController(AccountsController):
for d in self.items:
if not d.batch_no: continue
serial_nos = [sr.name for sr in frappe.get_all("Serial No",
{'batch_no': d.batch_no, 'status': 'Inactive'})]
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
d.batch_no = None
d.db_set("batch_no", None)
@@ -543,13 +507,41 @@ class StockController(AccountsController):
"voucher_no": self.name,
"company": self.company
})
if future_sle_exists(args):
if future_sle_exists(args) or repost_required_for_queue(self):
item_based_reposting = cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting"))
if item_based_reposting:
create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name)
else:
create_repost_item_valuation_entry(args)
def repost_required_for_queue(doc: StockController) -> bool:
"""check if stock document contains repeated item-warehouse with queue based valuation.
if queue exists for repeated items then SLEs need to reprocessed in background again.
"""
consuming_sles = frappe.db.get_all("Stock Ledger Entry",
filters={
"voucher_type": doc.doctype,
"voucher_no": doc.name,
"actual_qty": ("<", 0),
"is_cancelled": 0
},
fields=["item_code", "warehouse", "stock_queue"]
)
item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles]
unique_item_warehouses = set(item_warehouses)
if len(unique_item_warehouses) == len(item_warehouses):
return False
for sle in consuming_sles:
if sle.stock_queue != "[]": # using FIFO/LIFO valuation
return True
return False
@frappe.whitelist()
def make_quality_inspections(doctype, docname, items):

View File

@@ -363,8 +363,6 @@ class Subcontracting():
return
for row in self.get(self.raw_material_table):
self.__validate_consumed_qty(row)
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
@@ -372,12 +370,6 @@ class Subcontracting():
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_consumed_qty(self, row):
if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
def __validate_batch_no(self, row, key):
if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
link = get_link_to_form('Purchase Order', row.purchase_order)

View File

@@ -106,6 +106,9 @@ class calculate_taxes_and_totals(object):
self.doc.conversion_rate = flt(self.doc.conversion_rate)
def calculate_item_values(self):
if self.doc.get('is_consolidated'):
return
if not self.discount_amount_applied:
for item in self.doc.get("items"):
self.doc.round_floats_in(item)
@@ -139,6 +142,8 @@ class calculate_taxes_and_totals(object):
if not item.qty and self.doc.get("is_return"):
item.amount = flt(-1 * item.rate, item.precision("amount"))
elif not item.qty and self.doc.get("is_debit_note"):
item.amount = flt(item.rate, item.precision("amount"))
else:
item.amount = flt(item.rate * item.qty, item.precision("amount"))
@@ -265,7 +270,8 @@ class calculate_taxes_and_totals(object):
shipping_rule.apply(self.doc)
def calculate_taxes(self):
if not self.doc.get('is_consolidated'):
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
if not rounding_adjustment_computed:
self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx
@@ -321,7 +327,7 @@ class calculate_taxes_and_totals(object):
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
and self.doc.discount_amount \
and self.doc.apply_discount_on == "Grand Total" \
and not self.doc.get('is_consolidated'):
and not rounding_adjustment_computed:
self.doc.rounding_adjustment = flt(self.doc.grand_total
- flt(self.doc.discount_amount) - tax.total,
self.doc.precision("rounding_adjustment"))
@@ -460,20 +466,22 @@ class calculate_taxes_and_totals(object):
self.doc.total_net_weight += d.total_weight
def set_rounded_total(self):
if not self.doc.get('is_consolidated'):
if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = self.doc.base_rounded_total = 0
return
if self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment'):
return
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
self.doc.currency, self.doc.precision("rounded_total"))
if self.doc.meta.get_field("rounded_total"):
if self.doc.is_rounded_total_disabled():
self.doc.rounded_total = self.doc.base_rounded_total = 0
return
#if print_in_rate is set, we would have already calculated rounding adjustment
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
self.doc.precision("rounding_adjustment"))
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
self.doc.currency, self.doc.precision("rounded_total"))
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
#if print_in_rate is set, we would have already calculated rounding adjustment
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
self.doc.precision("rounding_adjustment"))
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
def _cleanup(self):
if not self.doc.get('is_consolidated'):
@@ -594,13 +602,14 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
if self.doc.party_account_currency == self.doc.currency:
total_amount_to_pay = flt(grand_total - self.doc.total_advance
- flt(self.doc.write_off_amount), self.doc.precision("grand_total"))
else:
total_amount_to_pay = flt(flt(grand_total *
self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance
- flt(self.doc.base_write_off_amount), self.doc.precision("grand_total"))
total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance
- flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total"))
self.doc.round_floats_in(self.doc, ["paid_amount"])
change_amount = 0
@@ -643,12 +652,12 @@ class calculate_taxes_and_totals(object):
def calculate_change_amount(self):
self.doc.change_amount = 0.0
self.doc.base_change_amount = 0.0
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
if self.doc.doctype == "Sales Invoice" \
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
and self.doc.paid_amount > grand_total and not self.doc.is_return \
and any(d.type == "Cash" for d in self.doc.payments):
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
self.doc.change_amount = flt(self.doc.paid_amount - grand_total +
self.doc.write_off_amount, self.doc.precision("change_amount"))

View File

@@ -1,6 +1,8 @@
import unittest
from functools import partial
import frappe
from erpnext.controllers import queries
@@ -54,6 +56,12 @@ class TestQueries(unittest.TestCase):
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
self.assertEqual(len(bundled_stock_items), 0)
# empty customer/supplier should be stripped of instead of failure
query(txt="", filters={"customer": None})
query(txt="", filters={"customer": ""})
query(txt="", filters={"supplier": None})
query(txt="", filters={"supplier": ""})
def test_bom_qury(self):
query = add_default_params(queries.bom, "BOM")
@@ -85,3 +93,6 @@ class TestQueries(unittest.TestCase):
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)
def test_default_uoms(self):
self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10)

View File

@@ -4,19 +4,72 @@ import frappe
class TestUtils(unittest.TestCase):
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)
def test_reset_default_field_value_in_mfg_stock_entry(self):
# manufacture stock entry with rows having blank source/target wh
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Manufacture",
stock_entry_type="Manufacture",
company="_Test Company",
from_warehouse="_Test Warehouse - _TC",
to_warehouse="_Test Warehouse 1 - _TC",
items=[
frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"),
frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1)
]
)
se.save()
# default fields must be untouched
self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC")
self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC")
se.delete()
def test_reset_default_field_value_in_transfer_stock_entry(self):
doc = frappe.get_doc({
"doctype": "Stock Entry",
"purpose": "Material Receipt",
"from_warehouse": "Warehouse 1",
"to_warehouse": "Warehouse 2",
})
# Same values
doc.items = [
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
]
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, "Warehouse 1")
self.assertEqual(doc.to_warehouse, "Warehouse 2")
# Mixed values in source wh
doc.items = [
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"},
{"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}
]
doc.reset_default_field_value("from_warehouse", "items", "s_warehouse")
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, None)
self.assertEqual(doc.to_warehouse, "Warehouse 2")

View File

@@ -3,7 +3,7 @@
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"allow_rename": 1,
"autoname": "field:lost_reason",
"beta": 0,
"creation": "2018-12-28 14:48:51.044975",
@@ -57,7 +57,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-12-28 14:49:43.336437",
"modified": "2022-02-16 10:49:43.336437",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity Lost Reason",
@@ -150,4 +150,4 @@
"track_changes": 0,
"track_seen": 0,
"track_views": 0
}
}

View File

@@ -1,7 +1,6 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import itertools
import json
import frappe
@@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator):
context.body_class = "product-page"
context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs
self.attributes = frappe.get_all("Item Variant Attribute",
self.attributes = frappe.get_all(
"Item Variant Attribute",
fields=["attribute", "attribute_value"],
filters={"parent": self.item_code})
filters={"parent": self.item_code}
)
if self.slideshow:
context.update(get_slideshow(self))
self.set_variant_context(context)
self.set_attribute_context(context)
self.set_disabled_attributes(context)
self.set_metatags(context)
self.set_shopping_cart_data(context)
@@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator):
return context
def set_variant_context(self, context):
if not self.has_variants:
return
context.no_cache = True
variant = frappe.form_dict.variant
# load variants
# also used in set_attribute_context
context.variants = frappe.get_all(
"Item",
filters={
"variant_of": self.item_code,
"published_in_website": 1
},
order_by="name asc")
# the case when the item is opened for the first time from its list
if not variant and context.variants:
variant = context.variants[0]
if variant:
context.variant = frappe.get_doc("Item", variant)
fields = ("website_image", "website_image_alt", "web_long_description", "description",
"website_specifications")
for fieldname in fields:
if context.variant.get(fieldname):
value = context.variant.get(fieldname)
if isinstance(value, list):
value = [d.as_dict() for d in value]
context[fieldname] = value
if self.slideshow and context.variant and context.variant.slideshow:
context.update(get_slideshow(context.variant))
def set_attribute_context(self, context):
if not self.has_variants:
return
attribute_values_available = {}
context.attribute_values = {}
context.selected_attributes = {}
# load attributes
self.set_selected_attributes(context.variants, context, attribute_values_available)
# filter attributes, order based on attribute table
item = frappe.get_cached_doc("Item", self.item_code)
self.set_attribute_values(item.attributes, context, attribute_values_available)
context.variant_info = json.dumps(context.variants)
def set_selected_attributes(self, variants, context, attribute_values_available):
for variant in variants:
variant.attributes = frappe.get_all(
@@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator):
if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []):
values.append(attr_value.attribute_value)
def set_disabled_attributes(self, context):
"""Disable selection options of attribute combinations that do not result in a variant"""
if not self.attributes or not self.has_variants:
return
context.disabled_attributes = {}
attributes = [attr.attribute for attr in self.attributes]
def find_variant(combination):
for variant in context.variants:
if len(variant.attributes) < len(attributes):
continue
if "combination" not in variant:
ref_combination = []
for attr in variant.attributes:
idx = attributes.index(attr.attribute)
ref_combination.insert(idx, attr.attribute_value)
variant["combination"] = ref_combination
if not (set(combination) - set(variant["combination"])):
# check if the combination is a subset of a variant combination
# eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5]
return True
for i, attr in enumerate(self.attributes):
if i == 0:
continue
combination_source = []
# loop through previous attributes
for prev_attr in self.attributes[:i]:
combination_source.append([context.selected_attributes.get(prev_attr.attribute)])
combination_source.append(context.attribute_values[attr.attribute])
for combination in itertools.product(*combination_source):
if not find_variant(combination):
context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1])
def set_metatags(self, context):
context.metatags = frappe._dict({})

View File

@@ -197,7 +197,10 @@ class ProductQuery:
website_item_groups = frappe.db.get_all(
"Website Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[["Website Item Group", "item_group", "=", item_group]]
filters=[
["Website Item Group", "item_group", "=", item_group],
["published", "=", 1]
]
)
return website_item_groups
@@ -262,7 +265,7 @@ class ProductQuery:
customer = get_customer(silent=True)
if customer:
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": customer, "order_type": "Shopping Cart", "docstatus": 0},
{"party_name": customer, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:
items = frappe.get_all(
@@ -296,4 +299,4 @@ class ProductQuery:
# slice results manually
result[:self.page_length]
return result
return result

View File

@@ -24,7 +24,7 @@ def set_cart_count(quotation=None):
if cint(frappe.db.get_singles_value("E Commerce Settings", "enabled")):
if not quotation:
quotation = _get_cart_quotation()
cart_count = cstr(len(quotation.get("items")))
cart_count = cstr(cint(quotation.get("total_qty")))
if hasattr(frappe.local, "cookie_manager"):
frappe.local.cookie_manager.set_cookie("cart_count", cart_count)
@@ -276,10 +276,29 @@ def guess_territory():
def decorate_quotation_doc(doc):
for d in doc.get("items", []):
item_code = d.item_code
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
# Variant Item
if not frappe.db.exists("Website Item", {"item_code": item_code}):
variant_data = frappe.db.get_values(
"Item",
filters={"item_code": item_code},
fieldname=["variant_of", "item_name", "image"],
as_dict=True
)[0]
item_code = variant_data.variant_of
fields = fields[1:]
d.web_item_name = variant_data.item_name
if variant_data.image: # get image from variant or template web item
d.thumbnail = variant_data.image
fields = fields[2:]
d.update(frappe.db.get_value(
"Website Item",
{"item_code": d.item_code},
["web_item_name", "thumbnail", "website_image", "description", "route"],
{"item_code": item_code},
fields,
as_dict=True)
)
@@ -292,7 +311,7 @@ def _get_cart_quotation(party=None):
party = get_party()
quotation = frappe.get_all("Quotation", fields=["name"], filters=
{"party_name": party.name, "order_type": "Shopping Cart", "docstatus": 0},
{"party_name": party.name, "contact_email": frappe.session.user, "order_type": "Shopping Cart", "docstatus": 0},
order_by="modified desc", limit_page_length=1)
if quotation:

View File

@@ -9,8 +9,13 @@ from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
from erpnext.tests.utils import create_test_contact_and_address
from erpnext.e_commerce.shopping_cart.cart import (
_get_cart_quotation,
get_cart_quotation,
get_party,
update_cart,
)
from erpnext.tests.utils import change_settings, create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
@@ -34,6 +39,7 @@ class TestShoppingCart(unittest.TestCase):
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
self.disable_shopping_cart()
@@ -50,13 +56,19 @@ class TestShoppingCart(unittest.TestCase):
return quotation
def test_get_cart_customer(self):
self.login_as_customer()
def validate_quotation():
# test if quotation with customer is fetched
quotation = _get_cart_quotation()
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.party_name, "_Test Customer")
self.assertEqual(quotation.contact_email, frappe.session.user)
return quotation
# test if quotation with customer is fetched
quotation = _get_cart_quotation()
self.assertEqual(quotation.quotation_to, "Customer")
self.assertEqual(quotation.party_name, "_Test Customer")
self.assertEqual(quotation.contact_email, frappe.session.user)
self.login_as_customer("test_contact_two_customer@example.com", "_Test Contact 2 For _Test Customer")
validate_quotation()
self.login_as_customer()
quotation = validate_quotation()
return quotation
@@ -128,6 +140,43 @@ class TestShoppingCart(unittest.TestCase):
self.remove_test_quotation(quotation)
@change_settings("E Commerce Settings",{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
def test_add_item_variant_without_web_item_to_cart(self):
"Test adding Variants having no Website Items in cart via Template Web Item."
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.stock.doctype.item.test_item import make_item
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
make_website_item(template_item) # publish template not variant
update_cart("Test-Tshirt-Temp-S-R", 1)
cart = get_cart_quotation() # test if cart page gets data without errors
doc = cart.get("doc")
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
# test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart)
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:
@@ -210,10 +259,9 @@ class TestShoppingCart(unittest.TestCase):
self.create_user_if_not_exists("test_cart_user@example.com")
frappe.set_user("test_cart_user@example.com")
def login_as_customer(self):
self.create_user_if_not_exists("test_contact_customer@example.com",
"_Test Contact For _Test Customer")
frappe.set_user("test_contact_customer@example.com")
def login_as_customer(self, email="test_contact_customer@example.com", name="_Test Contact For _Test Customer"):
self.create_user_if_not_exists(email, name)
frappe.set_user(email)
def clear_existing_quotations(self):
quotations = frappe.get_all("Quotation", filters={

View File

@@ -44,7 +44,7 @@ class ItemVariantsCacheManager:
val = frappe.cache().get_value('ordered_attribute_values_map')
if val: return val
all_attribute_values = frappe.db.get_all('Item Attribute Value',
all_attribute_values = frappe.get_all('Item Attribute Value',
['attribute_value', 'idx', 'parent'], order_by='idx asc')
ordered_attribute_values_map = frappe._dict({})
@@ -57,25 +57,32 @@ class ItemVariantsCacheManager:
def build_cache(self):
parent_item_code = self.item_code
attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute',
{'parent': parent_item_code}, ['attribute'], order_by='idx asc')
attributes = [
a.attribute for a in frappe.get_all(
'Item Variant Attribute',
{'parent': parent_item_code},
['attribute'],
order_by='idx asc'
)
]
item_variants_data = frappe.db.get_all('Item Variant Attribute',
{'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'],
order_by='name',
as_list=1
# Get Variants and tehir Attributes that are not disabled
iva = frappe.qb.DocType("Item Variant Attribute")
item = frappe.qb.DocType("Item")
query = (
frappe.qb.from_(iva)
.join(item).on(item.name == iva.parent)
.select(
iva.parent, iva.attribute, iva.attribute_value
).where(
(iva.variant_of == parent_item_code)
& (item.disabled == 0)
).orderby(iva.name)
)
item_variants_data = query.run()
unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])])
attribute_value_item_map = frappe._dict({})
item_attribute_value_map = frappe._dict({})
# dont consider variants that are unpublished
# (either have no Website Item or are unpublished in Website Item)
item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items]
item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})]
attribute_value_item_map = frappe._dict()
item_attribute_value_map = frappe._dict()
for row in item_variants_data:
item_code, attribute, attribute_value = row
@@ -115,4 +122,7 @@ def build_cache(item_code):
def enqueue_build_cache(item_code):
if frappe.cache().hget('item_cache_build_in_progress', item_code):
return
frappe.enqueue(build_cache, item_code=item_code, queue='long')
frappe.enqueue(
"erpnext.e_commerce.variant_selector.item_variants_cache.build_cache",
item_code=item_code, queue='long'
)

View File

@@ -1,11 +1,121 @@
# import frappe
import unittest
# from erpnext.e_commerce.product_data_engine.query import ProductQuery
# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
import frappe
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
setup_e_commerce_settings,
)
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Item"]
class TestVariantSelector(unittest.TestCase):
# TODO: Variant Selector Tests
pass
class TestVariantSelector(ERPNextTestCase):
@classmethod
def setUpClass(cls):
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
# create L-R, L-G, M-R, M-G and S-R
for size in ("Large", "Medium",):
for colour in ("Red", "Green",):
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": size, "Test Colour": colour
})
variant.save()
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
make_website_item(template_item) # publish template not variants
def test_item_attributes(self):
"""
Test if the right attributes are fetched in the popup.
(Attributes must only come from active items)
Attribute selection must not be linked to Website Items.
"""
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
self.assertEqual(attr_data[0]["attribute"], "Test Size")
self.assertEqual(attr_data[1]["attribute"], "Test Colour")
self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large']
self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green']
# disable small red tshirt, now there are no small tshirts.
# but there are some red tshirts
small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R")
small_variant.disabled = 1
small_variant.save() # trigger cache rebuild
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
# Only L and M attribute values must be fetched since S is disabled
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
# teardown
small_variant.disabled = 0
small_variant.save()
def test_next_item_variant_values(self):
"""
Test if on selecting an attribute value, the next possible values
are filtered accordingly.
Values that dont apply should not be fetched.
E.g.
There is a ** Small-Red ** Tshirt. No other colour in this size.
On selecting ** Small **, only ** Red ** should be selectable next.
"""
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
filtered_items = next_values["filtered_items"]
self.assertEqual(len(next_colours), 1)
self.assertEqual(next_colours.pop(), "Red")
self.assertEqual(len(filtered_items), 1)
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
def test_exact_match_with_price(self):
"""
Test price fetching and matching of variant without Website Item
"""
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
frappe.set_user("Administrator")
setup_e_commerce_settings({
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
frappe.local.shopping_cart_settings = None # clear cached settings values
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp",
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
)
print(">>>>", next_values)
price_info = next_values["product_info"]["price"]
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")

View File

@@ -1,7 +1,12 @@
import frappe
from frappe.utils import cint
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
from erpnext.utilities.product import get_price
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
@@ -143,14 +148,13 @@ def get_next_attribute_and_values(item_code, selected_attributes):
filtered_items_count = len(filtered_items)
# get product info if exact match
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
# from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
if exact_match:
data = get_product_info_for_website(exact_match[0])
product_info = data.product_info
cart_settings = get_shopping_cart_settings()
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
if product_info:
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price:
product_info = None
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
else:
product_info = None
@@ -195,3 +199,20 @@ def get_item_attributes(item_code):
return attributes
def get_item_variant_price_dict(item_code, cart_settings):
if cart_settings.enabled and cart_settings.show_price:
is_guest = frappe.session.user == "Guest"
# Show Price if logged in.
# If not logged in, check if price is hidden for guest.
if not is_guest or not cart_settings.hide_price_for_guest:
price_list = _set_price_list(cart_settings, None)
price = get_price(
item_code,
price_list,
cart_settings.default_customer_group,
cart_settings.company
)
return {"price": price}
return None

View File

@@ -201,8 +201,8 @@ def get_course_schedule_events(start, end, filters=None):
conditions = get_event_conditions("Course Schedule", filters)
data = frappe.db.sql("""select name, course, color,
timestamp(schedule_date, from_time) as from_datetime,
timestamp(schedule_date, to_time) as to_datetime,
timestamp(schedule_date, from_time) as from_time,
timestamp(schedule_date, to_time) as to_time,
room, student_group, 0 as 'allDay'
from `tabCourse Schedule`
where ( schedule_date between %(start)s and %(end)s )

View File

@@ -3,6 +3,8 @@
# For license information, please see license.txt
from datetime import datetime
import frappe
from frappe import _
from frappe.model.document import Document
@@ -30,6 +32,14 @@ class CourseSchedule(Document):
if self.from_time > self.to_time:
frappe.throw(_("From Time cannot be greater than To Time."))
"""Handles specicfic case to update schedule date in calendar """
if isinstance(self.from_time, str):
try:
datetime_obj = datetime.strptime(self.from_time, '%Y-%m-%d %H:%M:%S')
self.schedule_date = datetime_obj
except ValueError:
pass
def validate_overlap(self):
"""Validates overlap for Student Group, Instructor, Room"""
@@ -47,4 +57,4 @@ class CourseSchedule(Document):
validate_overlap_for(self, "Assessment Plan", "student_group")
validate_overlap_for(self, "Assessment Plan", "room")
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)
validate_overlap_for(self, "Assessment Plan", "supervisor", self.instructor)

View File

@@ -1,11 +1,10 @@
frappe.views.calendar["Course Schedule"] = {
field_map: {
// from_datetime and to_datetime don't exist as docfields but are used in onload
"start": "from_datetime",
"end": "to_datetime",
"start": "from_time",
"end": "to_time",
"id": "name",
"title": "course",
"allDay": "allDay"
"allDay": "allDay",
},
gantt: false,
order_by: "schedule_date",

View File

@@ -6,6 +6,7 @@ import unittest
import frappe
from frappe.utils import to_timedelta, today
from frappe.utils.data import add_to_date
from erpnext.education.utils import OverlapError
@@ -39,6 +40,11 @@ class TestCourseSchedule(unittest.TestCase):
make_course_schedule_test_record(from_time= cs1.from_time, to_time= cs1.to_time,
student_group="Course-TC102-2014-2015 (_Test Academic Term)", instructor="_Test Instructor 2", room=frappe.get_all("Room")[1].name)
def test_update_schedule_date(self):
doc = make_course_schedule_test_record(schedule_date= add_to_date(today(), days=1))
doc.schedule_date = add_to_date(doc.schedule_date, days=1)
doc.save()
def make_course_schedule_test_record(**args):
args = frappe._dict(args)

View File

@@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint
from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document
from frappe.query_builder.functions import Min
from frappe.utils import comma_and, get_link_to_form, getdate
@@ -60,8 +61,15 @@ class ProgramEnrollment(Document):
frappe.throw(_("Student is already enrolled."))
def update_student_joining_date(self):
date = frappe.db.sql("select min(enrollment_date) from `tabProgram Enrollment` where student= %s", self.student)
frappe.db.set_value("Student", self.student, "joining_date", date)
table = frappe.qb.DocType('Program Enrollment')
date = (
frappe.qb.from_(table)
.select(Min(table.enrollment_date).as_('enrollment_date'))
.where(table.student == self.student)
).run(as_dict=True)
if date:
frappe.db.set_value("Student", self.student, "joining_date", date[0].enrollment_date)
def make_fee_records(self):
from erpnext.education.api import get_fee_components

View File

@@ -1,2 +1,10 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Amazon MWS Settings', {
refresh: function (frm) {
let app_link = "<a href='https://github.com/frappe/ecommerce_integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Amazon MWS Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
}
});

View File

@@ -12,7 +12,7 @@ from six.moves.urllib.parse import urlencode
class GoCardlessSettings(Document):
supported_currencies = ["EUR", "DKK", "GBP", "SEK"]
supported_currencies = ["EUR", "DKK", "GBP", "SEK", "AUD", "NZD", "CAD", "USD"]
def validate(self):
self.initialize_client()
@@ -79,7 +79,7 @@ class GoCardlessSettings(Document):
def validate_transaction_currency(self, currency):
if currency not in self.supported_currencies:
frappe.throw(_("Please select another payment method. Stripe does not support transactions in currency '{0}'").format(currency))
frappe.throw(_("Please select another payment method. Go Cardless does not support transactions in currency '{0}'").format(currency))
def get_payment_url(self, **kwargs):
return get_url("./integrations/gocardless_checkout?{0}".format(urlencode(kwargs)))

View File

@@ -8,10 +8,6 @@ from frappe.utils import cint, flt
from erpnext import get_default_company, get_region
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
SUPPORTED_COUNTRY_CODES = ["AT", "AU", "BE", "BG", "CA", "CY", "CZ", "DE", "DK", "EE", "ES", "FI",
"FR", "GB", "GR", "HR", "HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO",
"SE", "SI", "SK", "US"]
@@ -35,12 +31,14 @@ def get_client():
if api_key and api_url:
client = taxjar.Client(api_key=api_key, api_url=api_url)
client.set_api_config('headers', {
'x-api-version': '2020-08-07'
'x-api-version': '2022-01-24'
})
return client
def create_transaction(doc, method):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
"""Create an order transaction in TaxJar"""
if not TAXJAR_CREATE_TRANSACTIONS:
@@ -51,6 +49,7 @@ def create_transaction(doc, method):
if not client:
return
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
sales_tax = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == TAX_ACCOUNT_HEAD])
if not sales_tax:
@@ -79,6 +78,7 @@ def create_transaction(doc, method):
def delete_transaction(doc, method):
"""Delete an existing TaxJar order transaction"""
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
if not TAXJAR_CREATE_TRANSACTIONS:
return
@@ -92,6 +92,8 @@ def delete_transaction(doc, method):
def get_tax_data(doc):
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
from_address = get_company_address_details(doc)
from_shipping_state = from_address.get("state")
from_country_code = frappe.db.get_value("Country", from_address.country, "code")
@@ -113,20 +115,20 @@ def get_tax_data(doc):
to_shipping_state = get_state_code(to_address, 'Shipping')
tax_dict = {
'from_country': from_country_code,
'from_zip': from_address.pincode,
'from_state': from_shipping_state,
'from_city': from_address.city,
'from_street': from_address.address_line1,
'to_country': to_country_code,
'to_zip': to_address.pincode,
'to_city': to_address.city,
'to_street': to_address.address_line1,
'to_state': to_shipping_state,
'shipping': shipping,
'amount': doc.net_total,
'plugin': 'erpnext',
'line_items': line_items
"from_country": from_country_code,
"from_zip": from_address.pincode,
"from_state": from_shipping_state,
"from_city": from_address.city,
"from_street": from_address.address_line1,
"to_country": to_country_code,
"to_zip": to_address.pincode,
"to_city": to_address.city,
"to_street": to_address.address_line1,
"to_state": to_shipping_state,
"shipping": shipping,
"amount": doc.net_total,
"plugin": "erpnext",
"line_items": line_items
}
return tax_dict
@@ -156,6 +158,9 @@ def get_line_item_dict(item, docstatus):
return tax_dict
def set_sales_tax(doc, method):
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
if not TAXJAR_CALCULATE_TAX:
return
@@ -206,6 +211,7 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals")
def check_for_nexus(doc, tax_dict):
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
for item in doc.get("items"):
item.tax_collectable = flt(0)
@@ -218,6 +224,8 @@ def check_for_nexus(doc, tax_dict):
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
or frappe.db.has_column("Customer", "exempt_from_sales_tax") \
and frappe.db.get_value("Customer", doc.customer, "exempt_from_sales_tax")

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