mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-07 15:25:19 +00:00
Merge branch 'version-13' of https://github.com/frappe/erpnext into enterprise-hotfix
This commit is contained in:
1
.flake8
1
.flake8
@@ -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
|
||||
|
||||
12
.github/workflows/patch.yml
vendored
12
.github/workflows/patch.yml
vendored
@@ -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
|
||||
|
||||
44
.github/workflows/server-tests.yml
vendored
44
.github/workflows/server-tests.yml
vendored
@@ -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 }}
|
||||
|
||||
4
.github/workflows/ui-tests.yml
vendored
4
.github/workflows/ui-tests.yml
vendored
@@ -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
|
||||
|
||||
@@ -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'''
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
|
||||
"withdrawal",
|
||||
"description",
|
||||
"reference_number",
|
||||
"bank_account"
|
||||
"bank_account",
|
||||
"currency"
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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))
|
||||
@@ -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
|
||||
|
||||
@@ -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`")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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-.#####'
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = []
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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")
|
||||
|
||||
52
erpnext/change_log/v13/v13_19_0.md
Normal file
52
erpnext/change_log/v13/v13_19_0.md
Normal 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))
|
||||
32
erpnext/change_log/v13/v13_20_0.md
Normal file
32
erpnext/change_log/v13/v13_20_0.md
Normal 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))
|
||||
49
erpnext/change_log/v13/v13_21_0.md
Normal file
49
erpnext/change_log/v13/v13_21_0.md
Normal 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))
|
||||
42
erpnext/change_log/v13/v13_22_0.md
Normal file
42
erpnext/change_log/v13/v13_22_0.md
Normal 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))
|
||||
@@ -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':
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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({})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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'
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 )
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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)))
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user